diff --git a/src/apis/external/useGetExternalName.ts b/src/apis/external/useGetExternalName.ts index 35619b3c..922452f6 100644 --- a/src/apis/external/useGetExternalName.ts +++ b/src/apis/external/useGetExternalName.ts @@ -17,9 +17,10 @@ const getExternalName = async (teamId: number): Promise => { } }; -export const useGetExternalName = (teamId: number) => { +export const useGetExternalName = (teamId: number, opts?: { enabled?: boolean }) => { return useQuery({ queryKey: [queryKey.EXTERNAL_NAME, teamId], queryFn: () => getExternalName(teamId), + enabled: (opts?.enabled ?? true) && Number.isFinite(teamId) && teamId > 0, }); }; diff --git a/src/apis/goal/useGetGoalName.ts b/src/apis/goal/useGetGoalName.ts index ba5a9b9a..9666402a 100644 --- a/src/apis/goal/useGetGoalName.ts +++ b/src/apis/goal/useGetGoalName.ts @@ -17,9 +17,10 @@ const getGoalName = async (teamId: number): Promise => { } }; -export const useGetGoalName = (teamId: number) => { +export const useGetGoalName = (teamId: number, opts?: { enabled?: boolean }) => { return useQuery({ queryKey: [queryKey.GOAL_NAME, teamId], queryFn: () => getGoalName(teamId), + enabled: (opts?.enabled ?? true) && Number.isFinite(teamId) && teamId > 0, }); }; diff --git a/src/apis/issue/useGetIssueName.ts b/src/apis/issue/useGetIssueName.ts index af4c3028..5d138d15 100644 --- a/src/apis/issue/useGetIssueName.ts +++ b/src/apis/issue/useGetIssueName.ts @@ -17,9 +17,10 @@ const getIssueName = async (teamId: number): Promise => { } }; -export const useGetIssueName = (teamId: number) => { +export const useGetIssueName = (teamId: number, opts?: { enabled?: boolean }) => { return useQuery({ queryKey: [queryKey.ISSUE_NAME, teamId], queryFn: () => getIssueName(teamId), + enabled: (opts?.enabled ?? true) && Number.isFinite(teamId) && teamId > 0, }); }; diff --git a/src/components/DetailView/DetailHeader.tsx b/src/components/DetailView/DetailHeader.tsx index bb2f5350..54cbaef7 100644 --- a/src/components/DetailView/DetailHeader.tsx +++ b/src/components/DetailView/DetailHeader.tsx @@ -1,7 +1,4 @@ import TeamIcon from '../ListView/TeamIcon'; -import { useGetGoalName } from '../../apis/goal/useGetGoalName.ts'; -import { useGetExternalName } from '../../apis/external/useGetExternalName.ts'; -import { useGetIssueName } from '../../apis/issue/useGetIssueName.ts'; import { useNavigate, useParams } from 'react-router-dom'; import { useGetWorkspaceTeams } from '../../apis/setting/useGetWorkspaceTeams.ts'; import { useMemo } from 'react'; @@ -10,17 +7,12 @@ interface DetailHeaderProps { type: 'goal' | 'issue' | 'external'; defaultTitle: string; // 상세페이지 제목에 아무것도 입력되지 않았을 때 기본 타이틀 title: string; // 상세페이지 제목으로부터 전달받아올 타이틀 + detailId?: string; // 상세페이지 id (부모에서 받아올 예정) } -const DetailHeader = ({ type, defaultTitle, title }: DetailHeaderProps) => { +const DetailHeader = ({ type, defaultTitle, title, detailId }: DetailHeaderProps) => { const navigate = useNavigate(); const teamId = Number(useParams<{ teamId: string }>().teamId); - const { data: detailId } = - type === 'goal' - ? useGetGoalName(teamId) - : type === 'issue' - ? useGetIssueName(teamId) - : useGetExternalName(teamId); // 팀 정보 불러오기 const { data: teamData } = useGetWorkspaceTeams(); @@ -28,17 +20,22 @@ const DetailHeader = ({ type, defaultTitle, title }: DetailHeaderProps) => { return teamData?.pages[0].teamList.find((team) => team.teamId === Number(teamId)); }, [teamData, teamId]); + const canNavigate = Boolean(currentTeam?.teamId); + return (
{/* 팀 아이콘, 팀명, props로 요소 전달 가능 */} navigate(`/workspace/team/${currentTeam?.teamId}/${type}`)} + onClick={() => { + if (!canNavigate) return; + navigate(`/workspace/team/${currentTeam?.teamId}/${type}`); + }} />
{/* 상세페이지 ID */} -
{detailId}
+
{detailId ?? ''}
{/** * 상세페이지 이름 * - 상세페이지 제목에 아무것도 입력되지 않았을 때는 defaultTitle(placeholder명과 동일) 렌더링 diff --git a/src/components/DetailView/WorkspaceDetailHeader.tsx b/src/components/DetailView/WorkspaceDetailHeader.tsx index 8365b79b..cd2cae89 100644 --- a/src/components/DetailView/WorkspaceDetailHeader.tsx +++ b/src/components/DetailView/WorkspaceDetailHeader.tsx @@ -1,8 +1,5 @@ import WorkspaceIcon from '../ListView/WorkspaceIcon'; import { useNavigate, useParams } from 'react-router-dom'; -import { useGetGoalName } from '../../apis/goal/useGetGoalName.ts'; -import { useGetIssueName } from '../../apis/issue/useGetIssueName.ts'; -import { useGetExternalName } from '../../apis/external/useGetExternalName.ts'; import { useGetWorkspaceTeams } from '../../apis/setting/useGetWorkspaceTeams.ts'; import { useMemo } from 'react'; @@ -10,17 +7,17 @@ interface WorkspaceDetailHeaderProps { type: 'goal' | 'issue' | 'external'; defaultTitle: string; // 상세페이지 제목에 아무것도 입력되지 않았을 때 기본 타이틀 title: string; // 상세페이지 제목으로부터 전달받아올 타이틀 + detailId?: string; } -const WorkspaceDetailHeader = ({ type, defaultTitle, title }: WorkspaceDetailHeaderProps) => { +const WorkspaceDetailHeader = ({ + type, + defaultTitle, + title, + detailId, +}: WorkspaceDetailHeaderProps) => { const navigate = useNavigate(); const teamId = Number(useParams<{ teamId: string }>().teamId); - const { data: detailId } = - type === 'goal' - ? useGetGoalName(teamId) - : type === 'issue' - ? useGetIssueName(teamId) - : useGetExternalName(teamId); // 팀 정보 불러오기 const { data: teamData } = useGetWorkspaceTeams(); @@ -28,17 +25,22 @@ const WorkspaceDetailHeader = ({ type, defaultTitle, title }: WorkspaceDetailHea return teamData?.pages[0].teamList.find((team) => team.teamId === Number(teamId)); }, [teamData, teamId]); + const canNavigate = Boolean(currentTeam?.teamId); + return (
{/* 워크스페이스 아이콘, 워크스페이스명, props로 요소 전달 가능 */} navigate(`/workspace/default/team/${currentTeam?.teamId}/${type}`)} + onClick={() => { + if (!canNavigate) return; + navigate(`/workspace/default/team/${currentTeam?.teamId}/${type}`); + }} />
{/* 상세페이지 ID */} -
{detailId}
+
{detailId ?? ''}
{/** * 상세페이지 이름 * - 상세페이지 제목에 아무것도 입력되지 않았을 때는 defaultTitle(placeholder명과 동일) 렌더링 diff --git a/src/pages/external/ExternalDetail.tsx b/src/pages/external/ExternalDetail.tsx index 81b9f2da..6e9eb70d 100644 --- a/src/pages/external/ExternalDetail.tsx +++ b/src/pages/external/ExternalDetail.tsx @@ -68,6 +68,7 @@ import { import { useToast } from '../../components/Toast/ToastProvider.tsx'; import { useModalActions, useModalInfo } from '../../hooks/useModal.ts'; import Modal from '../../components/Modal/Modal.tsx'; +import { useGetExternalName } from '../../apis/external/useGetExternalName.ts'; /** 상세페이지 모드 구분 * (1) create - 생성 모드: 처음에 생성하여 작성 완료하기 전 @@ -162,6 +163,15 @@ const ExternalDetail = ({ initialMode }: ExternalDetailProps) => { ] : []; + // 생성 모드에서만 "다음 생성될 이름"을 조회 + const isCreate = mode === 'create'; + const { data: nextGeneratedName } = useGetExternalName(teamId, { + enabled: isCreate, + }); + + // 헤더표시용 이름: 생성 모드면 nextGeneratedName, 그 외엔 조회 데이터의 name + const headerDetailName = isCreate ? nextGeneratedName : externalDetail?.name; + // 단일 선택 라벨 const selectedStatusLabel = STATUS_LABELS[state]; const selectedPriorityLabel = PRIORITY_LABELS[priority]; @@ -476,14 +486,19 @@ const ExternalDetail = ({ initialMode }: ExternalDetailProps) => { }; return ( -
+
{/* 상세페이지 헤더 */} - + {/* 상세페이지 메인 */} -
+
{/* 상세페이지 좌측 영역 - 제목 & 상세설명 & 댓글 */} -
+
{/* 상세페이지 제목 */} {
{/* 상세페이지 우측 영역 - 속성 탭 & 하단의 작성 완료 버튼 */} -
+
{/* 속성 탭 */}
속성
diff --git a/src/pages/goal/GoalDetail.tsx b/src/pages/goal/GoalDetail.tsx index b95a1142..d86a6793 100644 --- a/src/pages/goal/GoalDetail.tsx +++ b/src/pages/goal/GoalDetail.tsx @@ -53,6 +53,7 @@ import { queryKey } from '../../constants/queryKey'; import queryClient from '../../utils/queryClient'; import { useModalActions, useModalInfo } from '../../hooks/useModal'; import Modal from '../../components/Modal/Modal'; +import { useGetGoalName } from '../../apis/goal/useGetGoalName'; /** 상세페이지 모드 구분 * (1) create - 생성 모드: 처음에 생성하여 작성 완료하기 전 @@ -103,6 +104,15 @@ const GoalDetail = ({ initialMode }: GoalDetailProps) => { const canPatch = Number.isFinite(numericGoalId); // PATCH 가능 조건 const blocker = useBlocker(isEditable); // 편집하고 있는 상황에 화면 이동을 블로킹 + // 생성 모드에서만 "다음 생성될 이름"을 조회 + const isCreate = mode === 'create'; + const { data: nextGeneratedName } = useGetGoalName(teamId, { + enabled: isCreate, + }); + + // 헤더표시용 이름: 생성 모드면 nextGeneratedName, 그 외엔 조회 데이터의 name + const headerDetailName = isCreate ? nextGeneratedName : goalDetail?.name; + // 단일 선택 라벨 const selectedStatusLabel = STATUS_LABELS[state]; const selectedPriorityLabel = PRIORITY_LABELS[priority]; @@ -340,14 +350,19 @@ const GoalDetail = ({ initialMode }: GoalDetailProps) => { }; return ( -
+
{/* 상세페이지 헤더 */} - + {/* 상세페이지 메인 */} -
+
{/* 상세페이지 좌측 영역 - 제목 & 상세설명 & 댓글 */} -
+
{/* 상세페이지 제목 */} {
{/* 상세페이지 우측 영역 - 속성 탭 & 하단의 작성 완료 버튼 */} -
+
{/* 속성 탭 */}
속성
diff --git a/src/pages/issue/IssueDetail.tsx b/src/pages/issue/IssueDetail.tsx index 877e3d70..61d2577b 100644 --- a/src/pages/issue/IssueDetail.tsx +++ b/src/pages/issue/IssueDetail.tsx @@ -54,6 +54,7 @@ import { queryKey } from '../../constants/queryKey.ts'; import { useHydrateIssueDetail } from '../../hooks/useHydrateIssueDetail.ts'; import { useModalActions, useModalInfo } from '../../hooks/useModal.ts'; import Modal from '../../components/Modal/Modal.tsx'; +import { useGetIssueName } from '../../apis/issue/useGetIssueName.ts'; /** 상세페이지 모드 구분 * (1) create - 생성 모드: 처음에 생성하여 작성 완료하기 전 @@ -107,6 +108,15 @@ const IssueDetail = ({ initialMode }: IssueDetailProps) => { const canPatch = Number.isFinite(numericIssueId); // PATCH 가능 조건 const blocker = useBlocker(isEditable); // 편집하고 있는 상황에 화면 이동을 블로킹 + // 생성 모드에서만 "다음 생성될 이름"을 조회 + const isCreate = mode === 'create'; + const { data: nextGeneratedName } = useGetIssueName(teamId, { + enabled: isCreate, + }); + + // 헤더표시용 이름: 생성 모드면 nextGeneratedName, 그 외엔 조회 데이터의 name + const headerDetailName = isCreate ? nextGeneratedName : issueDetail?.name; + // 단일 선택 라벨 const selectedStatusLabel = STATUS_LABELS[state]; const selectedPriorityLabel = PRIORITY_LABELS[priority]; @@ -355,14 +365,19 @@ const IssueDetail = ({ initialMode }: IssueDetailProps) => { }; return ( -
+
{/* 상세페이지 헤더 */} - + {/* 상세페이지 메인 */} -
+
{/* 상세페이지 좌측 영역 - 제목 & 상세설명 & 댓글 */} -
+
{/* 상세페이지 제목 */} {
{/* 상세페이지 우측 영역 - 속성 탭 & 하단의 작성 완료 버튼 */} -
+
{/* 속성 탭 */}
속성
diff --git a/src/pages/workspace/WorkspaceExternalDetail.tsx b/src/pages/workspace/WorkspaceExternalDetail.tsx index e42e8b92..0ad7e872 100644 --- a/src/pages/workspace/WorkspaceExternalDetail.tsx +++ b/src/pages/workspace/WorkspaceExternalDetail.tsx @@ -68,6 +68,7 @@ import { useHydrateExternalDetail } from '../../hooks/useHydrateExternalDetail.t import { useModalActions, useModalInfo } from '../../hooks/useModal.ts'; import { useToast } from '../../components/Toast/ToastProvider.tsx'; import Modal from '../../components/Modal/Modal.tsx'; +import { useGetExternalName } from '../../apis/external/useGetExternalName.ts'; /** 상세페이지 모드 구분 * (1) create - 생성 모드: 처음에 생성하여 작성 완료하기 전 @@ -162,6 +163,15 @@ const WorkspaceExternalDetail = ({ initialMode }: WorkspaceExternalDetailProps) ] : []; + // 생성 모드에서만 "다음 생성될 이름"을 조회 + const isCreate = mode === 'create'; + const { data: nextGeneratedName } = useGetExternalName(teamId, { + enabled: isCreate, + }); + + // 헤더표시용 이름: 생성 모드면 nextGeneratedName, 그 외엔 조회 데이터의 name + const headerDetailName = isCreate ? nextGeneratedName : externalDetail?.name; + // 단일 선택 라벨 const selectedStatusLabel = STATUS_LABELS[state]; const selectedPriorityLabel = PRIORITY_LABELS[priority]; @@ -477,14 +487,19 @@ const WorkspaceExternalDetail = ({ initialMode }: WorkspaceExternalDetailProps) }; return ( -
+
{/* 상세페이지 헤더 */} - + {/* 상세페이지 메인 */} -
+
{/* 상세페이지 좌측 영역 - 제목 & 상세설명 & 댓글 */} -
+
{/* 상세페이지 제목 */} {/* 상세페이지 우측 영역 - 속성 탭 & 하단의 작성 완료 버튼 */} -
+
{/* 속성 탭 */}
속성
diff --git a/src/pages/workspace/WorkspaceGoalDetail.tsx b/src/pages/workspace/WorkspaceGoalDetail.tsx index c0a8e427..d69a0a55 100644 --- a/src/pages/workspace/WorkspaceGoalDetail.tsx +++ b/src/pages/workspace/WorkspaceGoalDetail.tsx @@ -53,6 +53,7 @@ import queryClient from '../../utils/queryClient'; import { queryKey } from '../../constants/queryKey'; import { useModalActions, useModalInfo } from '../../hooks/useModal'; import Modal from '../../components/Modal/Modal'; +import { useGetGoalName } from '../../apis/goal/useGetGoalName'; /** 상세페이지 모드 구분 * (1) create - 생성 모드: 처음에 생성하여 작성 완료하기 전 @@ -103,6 +104,15 @@ const WorkspaceGoalDetail = ({ initialMode }: WorkspaceGoalDetailProps) => { const canPatch = Number.isFinite(numericGoalId); // PATCH 가능 조건 const blocker = useBlocker(isEditable); // 편집하고 있는 상황에 화면 이동을 블로킹 + // 생성 모드에서만 "다음 생성될 이름"을 조회 + const isCreate = mode === 'create'; + const { data: nextGeneratedName } = useGetGoalName(teamId, { + enabled: isCreate, + }); + + // 헤더표시용 이름: 생성 모드면 nextGeneratedName, 그 외엔 조회 데이터의 name + const headerDetailName = isCreate ? nextGeneratedName : goalDetail?.name; + // 단일 선택 라벨 const selectedStatusLabel = STATUS_LABELS[state]; const selectedPriorityLabel = PRIORITY_LABELS[priority]; @@ -251,9 +261,6 @@ const WorkspaceGoalDetail = ({ initialMode }: WorkspaceGoalDetailProps) => { const handleCompletion = () => { if (!isCompleted) { // create 또는 edit 모드에서 view 모드로 전환하려는 시점 - if (Number.isFinite(numericGoalId)) { - queryClient.invalidateQueries({ queryKey: ['GOAL_DETAIL', numericGoalId] }); // 동일 goalId에서 view로 들어갈 때도 최신화 - } handleSubmit(); // 저장 성공 시 모드 전환 } else { handleToggleMode(); // 모드 전환 @@ -343,14 +350,19 @@ const WorkspaceGoalDetail = ({ initialMode }: WorkspaceGoalDetailProps) => { }; return ( -
+
{/* 상세페이지 헤더 */} - + {/* 상세페이지 메인 */} -
+
{/* 상세페이지 좌측 영역 - 제목 & 상세설명 & 댓글 */} -
+
{/* 상세페이지 제목 */} {
{/* 상세페이지 우측 영역 - 속성 탭 & 하단의 작성 완료 버튼 */} -
+
{/* 속성 탭 */}
속성
diff --git a/src/pages/workspace/WorkspaceIssueDetail.tsx b/src/pages/workspace/WorkspaceIssueDetail.tsx index 9be35d68..bf416049 100644 --- a/src/pages/workspace/WorkspaceIssueDetail.tsx +++ b/src/pages/workspace/WorkspaceIssueDetail.tsx @@ -54,6 +54,7 @@ import queryClient from '../../utils/queryClient.ts'; import { useHydrateIssueDetail } from '../../hooks/useHydrateIssueDetail.ts'; import { useModalActions, useModalInfo } from '../../hooks/useModal.ts'; import Modal from '../../components/Modal/Modal.tsx'; +import { useGetIssueName } from '../../apis/issue/useGetIssueName.ts'; /** 상세페이지 모드 구분 * (1) create - 생성 모드: 처음에 생성하여 작성 완료하기 전 @@ -107,6 +108,15 @@ const WorkspaceIssueDetail = ({ initialMode }: WorkspaceIssueDetailProps) => { const canPatch = Number.isFinite(numericIssueId); // PATCH 가능 조건 const blocker = useBlocker(isEditable); // 편집하고 있는 상황에 화면 이동을 블로킹 + // 생성 모드에서만 "다음 생성될 이름"을 조회 + const isCreate = mode === 'create'; + const { data: nextGeneratedName } = useGetIssueName(teamId, { + enabled: isCreate, + }); + + // 헤더표시용 이름: 생성 모드면 nextGeneratedName, 그 외엔 조회 데이터의 name + const headerDetailName = isCreate ? nextGeneratedName : issueDetail?.name; + // 단일 선택 라벨 const selectedStatusLabel = STATUS_LABELS[state]; const selectedPriorityLabel = PRIORITY_LABELS[priority]; @@ -355,14 +365,19 @@ const WorkspaceIssueDetail = ({ initialMode }: WorkspaceIssueDetailProps) => { }; return ( -
+
{/* 상세페이지 헤더 */} - + {/* 상세페이지 메인 */} -
+
{/* 상세페이지 좌측 영역 - 제목 & 상세설명 & 댓글 */} -
+
{/* 상세페이지 제목 */} {
{/* 상세페이지 우측 영역 - 속성 탭 & 하단의 작성 완료 버튼 */} -
+
{/* 속성 탭 */}
속성