Skip to content

Commit 4aee0ba

Browse files
authored
Merge pull request #182 from vecosystem/feature/#172-issue_detail-api
[FEAT] ์ด์Šˆ ์ž‘์„ฑ/์ƒ์„ธ์กฐํšŒ/์ˆ˜์ • API ์—ฐ๋™
2 parents 872c3dc + ea974fb commit 4aee0ba

14 files changed

Lines changed: 992 additions & 97 deletions

โ€Žsrc/apis/goal/useGetGoalDetail.tsโ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { ResponseViewGoalDetailDto, ViewGoalDetailDto } from '../../types/g
77
* ๋ชฉํ‘œ ์ƒ์„ธ ์กฐํšŒ ํ•จ์ˆ˜
88
* - ๋ชฉํ‘œ ์ƒ์„ธํŽ˜์ด์ง€ ์กฐํšŒ ๋ชจ๋“œ์—์„œ ์‚ฌ์šฉ
99
* - pages/goal/GoalDetail.tsx
10-
* - pages/goal/WorkspaceGoalDetail.tsx
10+
* - pages/workspace/WorkspaceGoalDetail.tsx
1111
*/
1212
const getGoalDetail = async (goalId: number): Promise<ViewGoalDetailDto> => {
1313
try {

โ€Žsrc/apis/goal/usePatchGoalDetail.tsโ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import queryClient from '../../utils/queryClient.ts';
1414
* ๋ชฉํ‘œ ์ˆ˜์ • (PATCH) ํ•จ์ˆ˜
1515
* - ๋™์ผ teamId / ๋™์ผ goalId ๋Œ€์ƒ์˜ ์ƒ์„ธ ๋‚ด์šฉ ๋ฐ˜์˜
1616
* - pages/goal/GoalDetail.tsx
17-
* - pages/goal/WorkspaceGoalDetail.tsx
17+
* - pages/workspace/WorkspaceGoalDetail.tsx
1818
*/
1919
const updateGoal = async (
2020
teamId: number,

โ€Žsrc/apis/goal/usePostCreateGoalDetail.tsโ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import queryClient from '../../utils/queryClient.ts';
1313
* ๋ชฉํ‘œ ์ž‘์„ฑ ํ•จ์ˆ˜
1414
* - ๋ชฉํ‘œ ์ƒ์„ธํŽ˜์ด์ง€ ์ƒ์„ฑ ๋ชจ๋“œ์—์„œ ์‚ฌ์šฉ
1515
* - pages/goal/GoalDetail.tsx
16-
* - pages/goal/WorkspaceGoalDetail.tsx
16+
* - pages/workspace/WorkspaceGoalDetail.tsx
1717
*/
1818
const createGoal = async (
1919
teamId: number,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { axiosInstance } from '../axios.ts';
2+
import { useQuery } from '@tanstack/react-query';
3+
import { queryKey } from '../../constants/queryKey.ts';
4+
import type { ResponseViewIssueDetailDto, ViewIssueDetailDto } from '../../types/issue.ts';
5+
6+
/**
7+
* ์ด์Šˆ ์ƒ์„ธ ์กฐํšŒ ํ•จ์ˆ˜
8+
* - ์ด์Šˆ ์ƒ์„ธํŽ˜์ด์ง€ ์กฐํšŒ ๋ชจ๋“œ์—์„œ ์‚ฌ์šฉ
9+
* - pages/issue/IssueDetail.tsx
10+
* - pages/workspace/WorkspaceIssueDetail.tsx
11+
*/
12+
const getIssueDetail = async (issueId: number): Promise<ViewIssueDetailDto> => {
13+
try {
14+
const { data } = await axiosInstance.get<ResponseViewIssueDetailDto>(`/api/issues/${issueId}`);
15+
if (!data.result) return Promise.reject(data);
16+
if (data?.isSuccess) {
17+
console.log('์กฐํšŒ ์„ฑ๊ณต:', data.result);
18+
}
19+
return data.result;
20+
} catch (error) {
21+
console.error('์ด์Šˆ ์ƒ์„ธ ์กฐํšŒ ์‹คํŒจ', error);
22+
throw error;
23+
}
24+
};
25+
26+
export const useGetIssueDetail = (issueId: number, opts?: { enabled?: boolean }) => {
27+
const enabled = (opts?.enabled ?? true) && Number.isFinite(issueId) && issueId > 0;
28+
29+
return useQuery<ViewIssueDetailDto>({
30+
queryKey: [queryKey.ISSUE_DETAIL, issueId],
31+
queryFn: () => getIssueDetail(issueId),
32+
enabled, // โ† create ๊ฒฝ๋กœ ๋“ฑ์—์„œ NaN/0์ด๋ฉด ์ฟผ๋ฆฌ ๋ฏธ์‹คํ–‰
33+
retry: (failureCount, error: any) => {
34+
if (error?.response?.status === 404) return false; // 404๋ฉด ์žฌ์‹œ๋„ ์•ˆํ•จ
35+
return failureCount < 2;
36+
},
37+
});
38+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { axiosInstance } from '../axios.ts';
2+
import { useMutation } from '@tanstack/react-query';
3+
import { mutationKey } from '../../constants/mutationKey.ts';
4+
import { queryKey } from '../../constants/queryKey.ts';
5+
import queryClient from '../../utils/queryClient.ts';
6+
import type {
7+
ResponseUpdateIssueDetailDto,
8+
UpdateIssueDetailDto,
9+
UpdateIssueResultDto,
10+
} from '../../types/issue.ts';
11+
12+
/**
13+
* ์ด์Šˆ ์ˆ˜์ • (PATCH) ํ•จ์ˆ˜
14+
* - ๋™์ผ teamId / ๋™์ผ issueId ๋Œ€์ƒ์˜ ์ƒ์„ธ ๋‚ด์šฉ ๋ฐ˜์˜
15+
* - pages/issue/IssueDetail.tsx
16+
* - pages/workspace/WorkspaceIssueDetail.tsx
17+
*/
18+
const updateIssue = async (
19+
teamId: number,
20+
issueId: number,
21+
payload: UpdateIssueDetailDto
22+
): Promise<UpdateIssueResultDto> => {
23+
try {
24+
const response = await axiosInstance.patch<ResponseUpdateIssueDetailDto>(
25+
`/api/teams/${teamId}/issues/${issueId}`,
26+
payload
27+
);
28+
29+
if (!response.data.result) return Promise.reject(response);
30+
return response.data.result;
31+
} catch (error: any) {
32+
console.error('์ด์Šˆ ์ˆ˜์ • ์‹คํŒจ:', error);
33+
console.log('๐Ÿ‘‰ RESPONSE STATUS:', error?.response?.status);
34+
console.log('๐Ÿ‘‰ RESPONSE DATA:', error?.response?.data);
35+
throw error;
36+
}
37+
};
38+
39+
export const useUpdateIssue = (teamId: number, issueId: number) => {
40+
return useMutation<UpdateIssueResultDto, Error, UpdateIssueDetailDto>({
41+
mutationKey: [mutationKey.ISSUE_UPDATE, teamId, issueId],
42+
mutationFn: (payload) => updateIssue(teamId, issueId, payload),
43+
onSuccess: () => {
44+
// ์ƒ์„ธ/๋ชฉ๋ก/๊ด€๋ จ ํŒŒ์ƒ ์ฟผ๋ฆฌ ์ตœ์‹ ํ™”
45+
queryClient.invalidateQueries({ queryKey: [queryKey.ISSUE_LIST, teamId] });
46+
queryClient.invalidateQueries({ queryKey: [queryKey.ISSUE_NAME, teamId] });
47+
},
48+
});
49+
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { axiosInstance } from '../axios.ts';
2+
import { useMutation } from '@tanstack/react-query';
3+
import { mutationKey } from '../../constants/mutationKey.ts';
4+
import { queryKey } from '../../constants/queryKey.ts';
5+
import queryClient from '../../utils/queryClient.ts';
6+
import type {
7+
CreateIssueDetailDto,
8+
CreateIssueResultDto,
9+
ResponseCreateIssueDetailDto,
10+
} from '../../types/issue.ts';
11+
12+
/**
13+
* ์ด์Šˆ ์ž‘์„ฑ ํ•จ์ˆ˜
14+
* - ์ด์Šˆ ์ƒ์„ธํŽ˜์ด์ง€ ์ƒ์„ฑ ๋ชจ๋“œ์—์„œ ์‚ฌ์šฉ
15+
* - pages/issue/IssueDetail.tsx
16+
* - pages/workspace/WorkspaceIssueDetail.tsx
17+
*/
18+
const createIssue = async (
19+
teamId: number,
20+
payload: CreateIssueDetailDto
21+
): Promise<CreateIssueResultDto> => {
22+
try {
23+
const response = await axiosInstance.post<ResponseCreateIssueDetailDto>(
24+
`/api/teams/${teamId}/issues`,
25+
payload
26+
);
27+
28+
if (!response.data.result) return Promise.reject(response);
29+
return response.data.result;
30+
} catch (error: any) {
31+
console.error('์ด์Šˆ ์ž‘์„ฑ ์‹คํŒจ:', error);
32+
console.log('๐Ÿ‘‰ RESPONSE STATUS:', error?.response?.status);
33+
console.log('๐Ÿ‘‰ RESPONSE DATA:', error?.response?.data);
34+
throw error;
35+
}
36+
};
37+
38+
export const useCreateIssue = (teamId: number) => {
39+
return useMutation<CreateIssueResultDto, Error, CreateIssueDetailDto>({
40+
mutationKey: [mutationKey.ISSUE_CREATE, teamId],
41+
mutationFn: (payload) => createIssue(teamId, payload),
42+
onSuccess: () => {
43+
// ์ด์Šˆ ์ž‘์„ฑํ•˜์—ฌ POST ํ›„ ์กฐํšŒ๋˜๋Š” ๋ฐ์ดํ„ฐ ์ตœ์‹ ํ™”
44+
queryClient.invalidateQueries({ queryKey: [queryKey.ISSUE_LIST, teamId] });
45+
queryClient.invalidateQueries({ queryKey: [queryKey.ISSUE_NAME, teamId] });
46+
},
47+
});
48+
};

โ€Žsrc/components/Dropdown/Dropdown.tsxโ€Ž

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import IcCheck from '../../assets/icons/check.svg';
22
import IcDownArrow from '../../assets/icons/down-arrow.svg';
33
import useDropdownRef from '../../hooks/useDropdownRef.ts';
4-
import { useCallback } from 'react';
4+
import { useCallback, useMemo } from 'react';
55

66
interface DropdownProps {
77
value?: string; // ํ˜„์žฌ ์„ ํƒ๋œ ๊ฐ’
@@ -22,6 +22,9 @@ const Dropdown = ({
2222
}: DropdownProps) => {
2323
const dropdownRef = useDropdownRef(onClose);
2424

25+
// ์ค‘๋ณต ๋ผ๋ฒจ ์ œ๊ฑฐ
26+
const safeOptions = useMemo(() => Array.from(new Set(options)), [options]);
27+
2528
const handleSelect = useCallback(
2629
(option: string) => {
2730
defaultValue && onSelect(option);
@@ -46,17 +49,17 @@ const Dropdown = ({
4649
<img src={IcDownArrow} alt={defaultValue} />
4750
</div>
4851
)}
49-
{options.map((option) => (
52+
{safeOptions.map((option, idx) => (
5053
<div
51-
key={option}
54+
key={`${option}__${idx}`} // ๊ณ ์œ  key ๋ณด์žฅ
5255
className={`flex py-[0.75rem] px-[1.2rem] ${value === option ? 'bg-gray-200' : ''}`}
5356
onClick={() => handleSelect(option)}
5457
>
5558
<span className={`font-xsmall-r text-gray-600 me-[0.4rem] truncate`}>{option}</span>
5659
<img
5760
className={`opacity-0 ${value === option ? 'opacity-100' : 'opacity-0'}`}
5861
src={IcCheck}
59-
alt={option}
62+
alt={`${option} ์„ ํƒ๋จ`}
6063
/>
6164
</div>
6265
))}

โ€Žsrc/hooks/useGoalDeadlinePatch.tsโ€Ž

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// src/hooks/useGoalDeadlinePatch.ts
21
import { useEffect, useRef, useCallback } from 'react';
32
import { buildDeadlinePatch } from '../utils/deadlinePatch';
43
import type { UpdateGoalDetailDto } from '../types/goal';
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// src/hooks/useHydrateIssueDetail.ts
2+
import { useEffect, useRef } from 'react';
3+
import type { SubmitHandleRef } from '../components/DetailView/TextEditor/lexical-plugins/SubmitHandlePlugin';
4+
import type { StatusCode, PriorityCode } from '../types/listItem';
5+
import type { ViewIssueDetailDto } from '../types/issue';
6+
import type { SimpleGoal } from '../types/goal';
7+
8+
type Params = {
9+
issueDetail?: ViewIssueDetailDto | undefined;
10+
issueId?: number;
11+
editorRef: React.RefObject<SubmitHandleRef | null>;
12+
13+
// ์™ธ๋ถ€ ์˜ต์…˜/๋งคํ•‘ ์ค€๋น„์—ฌ๋ถ€ ํŒ๋‹จ์šฉ
14+
workspaceMembers?: Array<{ memberId: number; name: string }>;
15+
simpleGoals?: SimpleGoal[]; // ๋ชฉํ‘œ ์—ฐ๊ฒฐ์šฉ ๊ฐ„๋‹จ ๋ชฉ๋ก
16+
nameToId: Record<string, number>;
17+
18+
// ์ƒํƒœ ์„ธํ„ฐ๋“ค
19+
setTitle: (v: string) => void;
20+
setState: (v: StatusCode) => void;
21+
setPriority: (v: PriorityCode) => void;
22+
setSelectedDate: (v: [Date | null, Date | null]) => void;
23+
setManagersId: (v: number[]) => void;
24+
setGoalId: (v: number | null) => void; // ๋‹จ์ผ ์„ ํƒ(์—†์Œ ๊ฐ€๋Šฅ)
25+
};
26+
27+
export const useHydrateIssueDetail = ({
28+
issueDetail,
29+
issueId,
30+
editorRef,
31+
workspaceMembers,
32+
simpleGoals,
33+
nameToId,
34+
setTitle,
35+
setState,
36+
setPriority,
37+
setSelectedDate,
38+
setManagersId,
39+
setGoalId,
40+
}: Params) => {
41+
const hydratedRef = useRef(false);
42+
43+
useEffect(() => {
44+
if (!issueDetail) return;
45+
if (!Number.isFinite(issueId)) return;
46+
if (hydratedRef.current) return;
47+
48+
// ์˜ต์…˜ ์ค€๋น„์—ฌ๋ถ€ ํŒ๋‹จ
49+
// - ๋‹ด๋‹น์ž๊ฐ€ ์กด์žฌํ•˜๋ฉด ๋ฉค๋ฒ„ ์˜ต์…˜ ์ค€๋น„ ํ•„์š”
50+
const membersReady =
51+
(workspaceMembers?.length ?? 0) > 0 || (issueDetail.managers?.cnt ?? 0) === 0;
52+
53+
// - ๋ชฉํ‘œ ์—ฐ๊ฒฐ(๋‹จ์ผ) ์„ธํŒ…์šฉ: ์„œ๋ฒ„ ์‘๋‹ต์— goal.id๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ”๋กœ ์„ธํŒ… ๊ฐ€๋Šฅ
54+
// goal.id๊ฐ€ ์—†๊ณ  title๋งŒ ์žˆ์„ ๊ฒฝ์šฐ, simpleGoals ์ค€๋น„ ํ›„ title->id ๋งคํ•‘ ํ•„์š”
55+
const needGoalsByTitle = !!issueDetail.goal?.title && issueDetail.goal?.id == null;
56+
const goalsReady = needGoalsByTitle ? (simpleGoals?.length ?? 0) > 0 : true;
57+
58+
if (!membersReady || !goalsReady) return;
59+
60+
// 1) ๊ธฐ๋ณธ ํ•„๋“œ
61+
setTitle(issueDetail.title ?? '');
62+
setState((issueDetail.state ?? 'NONE') as StatusCode);
63+
setPriority((issueDetail.priority ?? 'NONE') as PriorityCode);
64+
65+
// 2) ๊ธฐํ•œ
66+
const s = issueDetail.deadline?.start ? new Date(issueDetail.deadline.start) : null;
67+
const e = issueDetail.deadline?.end ? new Date(issueDetail.deadline.end) : null;
68+
setSelectedDate([s, e]);
69+
70+
// 3) ๋‹ด๋‹น์ž ids
71+
if ((issueDetail.managers?.cnt ?? 0) > 0) {
72+
const managerNames = issueDetail.managers?.info?.map((m) => m.name) ?? [];
73+
const ids = managerNames
74+
.map((n) => nameToId[n])
75+
.filter((v): v is number => typeof v === 'number');
76+
setManagersId(ids);
77+
} else {
78+
setManagersId([]);
79+
}
80+
81+
// 4) ๋ชฉํ‘œ goalId (๋‹จ์ผ)
82+
// - ์šฐ์„  ์‘๋‹ต์— id๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๊ฑธ ์‚ฌ์šฉ
83+
// - ์—†๊ณ  title๋งŒ ์žˆ์œผ๋ฉด simpleGoals์—์„œ title๋กœ ์ฐพ์•„ id ๋งคํ•‘
84+
// - ๋‘˜ ๋‹ค ์—†์œผ๋ฉด null
85+
if (issueDetail.goal?.id != null) {
86+
setGoalId(issueDetail.goal.id);
87+
} else if (issueDetail.goal?.title) {
88+
const map = new Map((simpleGoals ?? []).map((g) => [g.title, g.id] as const));
89+
const mapped = map.get(issueDetail.goal.title);
90+
setGoalId(typeof mapped === 'number' ? mapped : null);
91+
} else {
92+
setGoalId(null);
93+
}
94+
95+
// 5) ์—๋””ํ„ฐ ์—ญ์ง๋ ฌํ™”
96+
editorRef.current?.loadJson?.(issueDetail.content ?? '');
97+
98+
hydratedRef.current = true;
99+
}, [
100+
issueDetail,
101+
issueId,
102+
editorRef,
103+
workspaceMembers,
104+
simpleGoals,
105+
nameToId,
106+
setTitle,
107+
setState,
108+
setPriority,
109+
setSelectedDate,
110+
setManagersId,
111+
setGoalId,
112+
]);
113+
};

0 commit comments

Comments
ย (0)