Initial deployment setup
Deploy staging / deploy (push) Failing after 6s

This commit is contained in:
root
2026-08-31 16:45:24 +09:00
commit 33453ecc55
3475 changed files with 850363 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
export * from './issue.type';
export * from './ui';
export * from './lib';
@@ -0,0 +1,51 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import { z } from 'zod';
import { categorySchema } from '../category/category.schema';
export const issueSchema = z.object({
id: z.number(),
name: z.string().trim().min(1).max(30),
description: z.string().trim().max(50).nullable(),
feedbackCount: z.number(),
issueAdminUserId: z.number().nullable().optional(),
status: z.union([
z.literal('INIT'),
z.literal('ON_REVIEW'),
z.literal('DETAILED_REVIEW'),
z.literal('IN_PROGRESS'),
z.literal('RESOLVED'),
z.literal('PENDING'),
]),
externalIssueId: z.string().trim().optional(),
externalIssueUrl: z.string().url().nullable().optional(),
externalIssueStatus: z.string().nullable().optional(),
externalIssueSyncStatus: z.string().optional(),
externalIssueSyncError: z.string().nullable().optional(),
externalIssueSyncedAt: z.string().nullable().optional(),
createdAt: z.string(),
updatedAt: z.string().nullable(),
category: categorySchema.nullable(),
});
export const issueFormSchema = issueSchema.pick({
name: true,
description: true,
status: true,
externalIssueId: true,
});
+22
View File
@@ -0,0 +1,22 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import type { z } from 'zod';
import type { issueFormSchema, issueSchema } from './issue.schema';
export type Issue = z.infer<typeof issueSchema>;
export type IssueStatus = Issue['status'];
export type IssueFormSchema = z.infer<typeof issueFormSchema>;
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
export { default as useIssueSearch } from './use-issue-search';
export { default as useIssueSearchInfinite } from './use-issue-search-infinite';
@@ -0,0 +1,88 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import { useInfiniteQuery } from '@tanstack/react-query';
import type {
OAIMutationResponse,
OAIRequestBody,
SearchQuery,
} from '@/shared';
import { client } from '@/shared';
type TData = OAIMutationResponse<
'/api/admin/projects/{projectId}/issues/search',
'post'
>;
interface IBody
extends Omit<
OAIRequestBody<'/api/admin/projects/{projectId}/issues/search', 'post'>,
'queries'
> {
queries: SearchQuery[];
}
const useIssueSearchInfinite = (
projectId: number,
body: IBody = { limit: 20, page: 1, queries: [], sort: {} },
) => {
return useInfiniteQuery<TData>({
queryKey: [
'/api/admin/projects/{projectId}/issues/search',
projectId,
body,
],
queryFn: async ({ pageParam }) => {
const { data: result } = await client.post({
path: '/api/admin/projects/{projectId}/issues/search',
pathParams: { projectId },
body: { ...body, page: pageParam as number },
});
return result;
},
getNextPageParam: (lastPage) => {
if (!lastPage) return undefined;
if (lastPage.meta.currentPage < lastPage.meta.totalPages) {
return lastPage.meta.currentPage + 1;
}
return undefined;
},
initialPageParam: 1,
// Kanban renders one query per status column. Keep the results warm so
// opening an issue detail sheet does not refetch all status columns.
staleTime: 30_000,
refetchOnWindowFocus: false,
// The placeholder below must always trigger a real fetch on first mount.
initialDataUpdatedAt: 0,
initialData: {
pageParams: [],
pages: [
{
items: [],
meta: {
currentPage: 1,
totalPages: 0,
totalItems: 0,
itemCount: 0,
itemsPerPage: 0,
},
},
],
},
});
};
export default useIssueSearchInfinite;
@@ -0,0 +1,62 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import type {
OAIMutationResponse,
OAIRequestBody,
SearchQuery,
} from '@/shared';
import { client } from '@/shared';
type TData = OAIMutationResponse<
'/api/admin/projects/{projectId}/issues/search',
'post'
>;
interface IBody
extends Omit<
OAIRequestBody<'/api/admin/projects/{projectId}/issues/search', 'post'>,
'queries'
> {
queries: SearchQuery[];
}
const useIssueSearch = (
projectId: number,
body: IBody = { limit: 10, page: 1, queries: [], sort: {} },
options?: Omit<UseQueryOptions<TData>, 'queryKey' | 'queryFn'>,
) => {
return useQuery<TData>({
queryKey: [
'/api/admin/projects/{projectId}/issues/search',
projectId,
body,
],
queryFn: async () => {
const { data: result } = await client.post({
path: '/api/admin/projects/{projectId}/issues/search',
pathParams: { projectId },
body,
});
return result;
},
...options,
});
};
export default useIssueSearch;
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
export { default as IssueBadge } from './issue-badge.ui';
export { default as IssueSelectBox } from './issue-select-box.ui';
@@ -0,0 +1,51 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import { useTranslation } from 'next-i18next';
import type { BadgeProps } from '@ufb/react';
import { Badge } from '@ufb/react';
import { ISSUES } from '@/shared';
import { BADGE_COLOR_MAP } from '@/shared/constants/color-map';
import type { BadgeColor } from '@/shared/constants/color-map';
import type { Issue, IssueStatus } from '../issue.type';
const ISSUE_COLOR_MAP: Record<IssueStatus, BadgeColor> = {
INIT: 'yellow',
ON_REVIEW: 'green',
DETAILED_REVIEW: 'orange',
IN_PROGRESS: 'sky',
RESOLVED: 'zinc',
PENDING: 'indigo',
};
interface IProps extends Omit<BadgeProps, 'color'> {
right?: React.ReactNode;
status: Issue['status'];
name?: string;
}
const IssueBadge: React.FC<IProps> = ({ name, status, right, ...props }) => {
const { t } = useTranslation();
return (
<Badge {...props} className={BADGE_COLOR_MAP[ISSUE_COLOR_MAP[status]]}>
{name ?? ISSUES(t).find((v) => v.key === status)?.name} {right}
</Badge>
);
};
export default IssueBadge;
@@ -0,0 +1,91 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { useThrottle } from 'react-use';
import { client } from '@/shared';
import AsyncMultiSelectSearchInput from '@/shared/ui/inputs/async-multi-select-search-input.ui';
import { useIssueSearchInfinite } from '../lib';
interface Props {
onChange: (value?: number[]) => void;
value?: number[];
}
const IssueSelectBox = ({ onChange, value }: Props) => {
const router = useRouter();
const projectId = Number(router.query.projectId as string);
const [selectedIssues, setSelectedIssues] = useState<
{ label: string; value: string }[]
>([]);
const [inputValue, setInputValue] = useState('');
const throttedValue = useThrottle(inputValue, 500);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useIssueSearchInfinite(projectId, {
queries: [{ key: 'name', value: throttedValue, condition: 'CONTAINS' }],
sort: { name: 'ASC' },
});
useEffect(() => {
if (!value) {
setSelectedIssues([]);
return;
}
void Promise.allSettled(
value.map(async (issueId) => {
try {
const { data } = await client.get({
path: '/api/admin/projects/{projectId}/issues/{issueId}',
pathParams: { projectId, issueId },
});
return { label: data.name, value: String(issueId) };
} catch (error) {
console.warn(`Failed to fetch issue ${issueId}:`, error);
return { label: `Issue ${issueId}`, value: String(issueId) };
}
}),
).then((results) => {
const successfulResults = results
.filter((result) => result.status === 'fulfilled')
.map((result) => result.value);
setSelectedIssues(successfulResults);
});
}, [value]);
return (
<AsyncMultiSelectSearchInput
options={data.pages.flatMap((page) =>
page ?
page.items.map(({ name, id }) => ({ label: name, value: String(id) }))
: [],
)}
value={selectedIssues}
onChange={(input) => onChange(input.map((v) => Number(v.value)))}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
inputValue={inputValue}
setInputValue={setInputValue}
isFetchingNextPage={isFetchingNextPage}
/>
);
};
export default IssueSelectBox;