Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/apis/goal/useGetGoalDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { ResponseViewGoalDetailDto, ViewGoalDetailDto } from '../../types/g
* 목표 상세 조회 함수
* - 목표 상세페이지 조회 모드에서 사용
* - pages/goal/GoalDetail.tsx
* - pages/goal/WorkspaceGoalDetail.tsx
* - pages/workspace/WorkspaceGoalDetail.tsx
*/
const getGoalDetail = async (goalId: number): Promise<ViewGoalDetailDto> => {
try {
Expand Down
2 changes: 1 addition & 1 deletion src/apis/goal/usePatchGoalDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import queryClient from '../../utils/queryClient.ts';
* 목표 수정 (PATCH) 함수
* - 동일 teamId / 동일 goalId 대상의 상세 내용 반영
* - pages/goal/GoalDetail.tsx
* - pages/goal/WorkspaceGoalDetail.tsx
* - pages/workspace/WorkspaceGoalDetail.tsx
*/
const updateGoal = async (
teamId: number,
Expand Down
2 changes: 1 addition & 1 deletion src/apis/goal/usePostCreateGoalDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import queryClient from '../../utils/queryClient.ts';
* 목표 작성 함수
* - 목표 상세페이지 생성 모드에서 사용
* - pages/goal/GoalDetail.tsx
* - pages/goal/WorkspaceGoalDetail.tsx
* - pages/workspace/WorkspaceGoalDetail.tsx
*/
const createGoal = async (
teamId: number,
Expand Down
38 changes: 38 additions & 0 deletions src/apis/issue/useGetIssueDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { axiosInstance } from '../axios.ts';
import { useQuery } from '@tanstack/react-query';
import { queryKey } from '../../constants/queryKey.ts';
import type { ResponseViewIssueDetailDto, ViewIssueDetailDto } from '../../types/issue.ts';

/**
* 이슈 상세 조회 함수
* - 이슈 상세페이지 조회 모드에서 사용
* - pages/issue/IssueDetail.tsx
* - pages/workspace/WorkspaceIssueDetail.tsx
*/
const getIssueDetail = async (issueId: number): Promise<ViewIssueDetailDto> => {
try {
const { data } = await axiosInstance.get<ResponseViewIssueDetailDto>(`/api/issues/${issueId}`);
if (!data.result) return Promise.reject(data);
if (data?.isSuccess) {
console.log('조회 성공:', data.result);
}
return data.result;
} catch (error) {
console.error('이슈 상세 조회 실패', error);
throw error;
}
};

export const useGetIssueDetail = (issueId: number, opts?: { enabled?: boolean }) => {
const enabled = (opts?.enabled ?? true) && Number.isFinite(issueId) && issueId > 0;

return useQuery<ViewIssueDetailDto>({
queryKey: [queryKey.ISSUE_DETAIL, issueId],
queryFn: () => getIssueDetail(issueId),
enabled, // ← create 경로 등에서 NaN/0이면 쿼리 미실행
retry: (failureCount, error: any) => {
if (error?.response?.status === 404) return false; // 404면 재시도 안함
return failureCount < 2;
},
});
};
49 changes: 49 additions & 0 deletions src/apis/issue/usePatchIssueDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { axiosInstance } from '../axios.ts';
import { useMutation } from '@tanstack/react-query';
import { mutationKey } from '../../constants/mutationKey.ts';
import { queryKey } from '../../constants/queryKey.ts';
import queryClient from '../../utils/queryClient.ts';
import type {
ResponseUpdateIssueDetailDto,
UpdateIssueDetailDto,
UpdateIssueResultDto,
} from '../../types/issue.ts';

/**
* 이슈 수정 (PATCH) 함수
* - 동일 teamId / 동일 issueId 대상의 상세 내용 반영
* - pages/issue/IssueDetail.tsx
* - pages/workspace/WorkspaceIssueDetail.tsx
*/
const updateIssue = async (
teamId: number,
issueId: number,
payload: UpdateIssueDetailDto
): Promise<UpdateIssueResultDto> => {
try {
const response = await axiosInstance.patch<ResponseUpdateIssueDetailDto>(
`/api/teams/${teamId}/issues/${issueId}`,
payload
);

if (!response.data.result) return Promise.reject(response);
return response.data.result;
} catch (error: any) {
console.error('이슈 수정 실패:', error);
console.log('👉 RESPONSE STATUS:', error?.response?.status);
console.log('👉 RESPONSE DATA:', error?.response?.data);
throw error;
}
};

export const useUpdateIssue = (teamId: number, issueId: number) => {
return useMutation<UpdateIssueResultDto, Error, UpdateIssueDetailDto>({
mutationKey: [mutationKey.ISSUE_UPDATE, teamId, issueId],
mutationFn: (payload) => updateIssue(teamId, issueId, payload),
onSuccess: () => {
// 상세/목록/관련 파생 쿼리 최신화
queryClient.invalidateQueries({ queryKey: [queryKey.ISSUE_LIST, teamId] });
queryClient.invalidateQueries({ queryKey: [queryKey.ISSUE_NAME, teamId] });
},
});
};
48 changes: 48 additions & 0 deletions src/apis/issue/usePostCreateIssueDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { axiosInstance } from '../axios.ts';
import { useMutation } from '@tanstack/react-query';
import { mutationKey } from '../../constants/mutationKey.ts';
import { queryKey } from '../../constants/queryKey.ts';
import queryClient from '../../utils/queryClient.ts';
import type {
CreateIssueDetailDto,
CreateIssueResultDto,
ResponseCreateIssueDetailDto,
} from '../../types/issue.ts';

/**
* 이슈 작성 함수
* - 이슈 상세페이지 생성 모드에서 사용
* - pages/issue/IssueDetail.tsx
* - pages/workspace/WorkspaceIssueDetail.tsx
*/
const createIssue = async (
teamId: number,
payload: CreateIssueDetailDto
): Promise<CreateIssueResultDto> => {
try {
const response = await axiosInstance.post<ResponseCreateIssueDetailDto>(
`/api/teams/${teamId}/issues`,
payload
);

if (!response.data.result) return Promise.reject(response);
return response.data.result;
} catch (error: any) {
console.error('이슈 작성 실패:', error);
console.log('👉 RESPONSE STATUS:', error?.response?.status);
console.log('👉 RESPONSE DATA:', error?.response?.data);
throw error;
}
};

export const useCreateIssue = (teamId: number) => {
return useMutation<CreateIssueResultDto, Error, CreateIssueDetailDto>({
mutationKey: [mutationKey.ISSUE_CREATE, teamId],
mutationFn: (payload) => createIssue(teamId, payload),
onSuccess: () => {
// 이슈 작성하여 POST 후 조회되는 데이터 최신화
queryClient.invalidateQueries({ queryKey: [queryKey.ISSUE_LIST, teamId] });
queryClient.invalidateQueries({ queryKey: [queryKey.ISSUE_NAME, teamId] });
},
});
};
11 changes: 7 additions & 4 deletions src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import IcCheck from '../../assets/icons/check.svg';
import IcDownArrow from '../../assets/icons/down-arrow.svg';
import useDropdownRef from '../../hooks/useDropdownRef.ts';
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';

interface DropdownProps {
value?: string; // 현재 선택된 값
Expand All @@ -22,6 +22,9 @@ const Dropdown = ({
}: DropdownProps) => {
const dropdownRef = useDropdownRef(onClose);

// 중복 라벨 제거
const safeOptions = useMemo(() => Array.from(new Set(options)), [options]);

const handleSelect = useCallback(
(option: string) => {
defaultValue && onSelect(option);
Expand All @@ -46,17 +49,17 @@ const Dropdown = ({
<img src={IcDownArrow} alt={defaultValue} />
</div>
)}
{options.map((option) => (
{safeOptions.map((option, idx) => (
<div
key={option}
key={`${option}__${idx}`} // 고유 key 보장
className={`flex py-[0.75rem] px-[1.2rem] ${value === option ? 'bg-gray-200' : ''}`}
onClick={() => handleSelect(option)}
>
<span className={`font-xsmall-r text-gray-600 me-[0.4rem] truncate`}>{option}</span>
<img
className={`opacity-0 ${value === option ? 'opacity-100' : 'opacity-0'}`}
src={IcCheck}
alt={option}
alt={`${option} 선택됨`}
/>
</div>
))}
Expand Down
1 change: 0 additions & 1 deletion src/hooks/useGoalDeadlinePatch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// src/hooks/useGoalDeadlinePatch.ts
import { useEffect, useRef, useCallback } from 'react';
import { buildDeadlinePatch } from '../utils/deadlinePatch';
import type { UpdateGoalDetailDto } from '../types/goal';
Expand Down
113 changes: 113 additions & 0 deletions src/hooks/useHydrateIssueDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// src/hooks/useHydrateIssueDetail.ts
import { useEffect, useRef } from 'react';
import type { SubmitHandleRef } from '../components/DetailView/TextEditor/lexical-plugins/SubmitHandlePlugin';
import type { StatusCode, PriorityCode } from '../types/listItem';
import type { ViewIssueDetailDto } from '../types/issue';
import type { SimpleGoal } from '../types/goal';

type Params = {
issueDetail?: ViewIssueDetailDto | undefined;
issueId?: number;
editorRef: React.RefObject<SubmitHandleRef | null>;

// 외부 옵션/매핑 준비여부 판단용
workspaceMembers?: Array<{ memberId: number; name: string }>;
simpleGoals?: SimpleGoal[]; // 목표 연결용 간단 목록
nameToId: Record<string, number>;

// 상태 세터들
setTitle: (v: string) => void;
setState: (v: StatusCode) => void;
setPriority: (v: PriorityCode) => void;
setSelectedDate: (v: [Date | null, Date | null]) => void;
setManagersId: (v: number[]) => void;
setGoalId: (v: number | null) => void; // 단일 선택(없음 가능)
};

export const useHydrateIssueDetail = ({
issueDetail,
issueId,
editorRef,
workspaceMembers,
simpleGoals,
nameToId,
setTitle,
setState,
setPriority,
setSelectedDate,
setManagersId,
setGoalId,
}: Params) => {
const hydratedRef = useRef(false);

useEffect(() => {
if (!issueDetail) return;
if (!Number.isFinite(issueId)) return;
if (hydratedRef.current) return;

// 옵션 준비여부 판단
// - 담당자가 존재하면 멤버 옵션 준비 필요
const membersReady =
(workspaceMembers?.length ?? 0) > 0 || (issueDetail.managers?.cnt ?? 0) === 0;

// - 목표 연결(단일) 세팅용: 서버 응답에 goal.id가 있으면 바로 세팅 가능
// goal.id가 없고 title만 있을 경우, simpleGoals 준비 후 title->id 매핑 필요
const needGoalsByTitle = !!issueDetail.goal?.title && issueDetail.goal?.id == null;
const goalsReady = needGoalsByTitle ? (simpleGoals?.length ?? 0) > 0 : true;

if (!membersReady || !goalsReady) return;

// 1) 기본 필드
setTitle(issueDetail.title ?? '');
setState((issueDetail.state ?? 'NONE') as StatusCode);
setPriority((issueDetail.priority ?? 'NONE') as PriorityCode);

// 2) 기한
const s = issueDetail.deadline?.start ? new Date(issueDetail.deadline.start) : null;
const e = issueDetail.deadline?.end ? new Date(issueDetail.deadline.end) : null;
setSelectedDate([s, e]);

// 3) 담당자 ids
if ((issueDetail.managers?.cnt ?? 0) > 0) {
const managerNames = issueDetail.managers?.info?.map((m) => m.name) ?? [];
const ids = managerNames
.map((n) => nameToId[n])
.filter((v): v is number => typeof v === 'number');
setManagersId(ids);
} else {
setManagersId([]);
}

// 4) 목표 goalId (단일)
// - 우선 응답에 id가 있으면 그걸 사용
// - 없고 title만 있으면 simpleGoals에서 title로 찾아 id 매핑
// - 둘 다 없으면 null
if (issueDetail.goal?.id != null) {
setGoalId(issueDetail.goal.id);
} else if (issueDetail.goal?.title) {
const map = new Map((simpleGoals ?? []).map((g) => [g.title, g.id] as const));
const mapped = map.get(issueDetail.goal.title);
setGoalId(typeof mapped === 'number' ? mapped : null);
} else {
setGoalId(null);
}

// 5) 에디터 역직렬화
editorRef.current?.loadJson?.(issueDetail.content ?? '');

hydratedRef.current = true;
}, [
issueDetail,
issueId,
editorRef,
workspaceMembers,
simpleGoals,
nameToId,
setTitle,
setState,
setPriority,
setSelectedDate,
setManagersId,
setGoalId,
]);
};
Loading