diff --git a/src/App.tsx b/src/App.tsx index 158b7d3..6abb819 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,8 @@ import { hasActiveSprint } from '@lib/api/sprint'; import { callApi } from '@lib/apiClient'; import UserInfoPage from '@pages/UserInfoPage'; import StopCommentPage from '@pages/StopCommentPage'; +import StartCommentPage from '@pages/StartCommentPage'; +import ContinueCommentPage from '@pages/ContinueCommentPage'; const router = createBrowserRouter([ { @@ -24,6 +26,8 @@ const router = createBrowserRouter([ { path: '/sprint-intro', element: }, { path: '/user-info', element: }, { path: '/stop-comment', element: }, + { path: '/start-comment', element: }, + { path: '/continue-comment', element: } ]); function App() { diff --git a/src/assets/continue_comment_example.svg b/src/assets/continue_comment_example.svg new file mode 100644 index 0000000..9c71c33 --- /dev/null +++ b/src/assets/continue_comment_example.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/continue_comment_explanation.svg b/src/assets/continue_comment_explanation.svg new file mode 100644 index 0000000..4b05e41 --- /dev/null +++ b/src/assets/continue_comment_explanation.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/start_comment_example.svg b/src/assets/start_comment_example.svg new file mode 100644 index 0000000..b358e19 --- /dev/null +++ b/src/assets/start_comment_example.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/start_comment_explanation.svg b/src/assets/start_comment_explanation.svg new file mode 100644 index 0000000..2c1823a --- /dev/null +++ b/src/assets/start_comment_explanation.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/index.ts b/src/components/index.ts index 0fd2267..953d153 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -9,4 +9,4 @@ export { default as InputField } from './common/form/InputField'; export { default as SprintCodeInput, SPRINT_CODE_LENGTH } from './sprint-code/SprintCodeInput'; export { default as PeerCommentRepeater } from './peer-comment/PeerCommentRepeater'; export { default as PeerCommentStepTemplate } from './peer-comment/PeerCommentStepTemplate'; -export type { PeerCommentStepTemplateProps, PeerCommentStepContent } from './peer-comment/PeerCommentStepTemplate'; +export type { PeerCommentStepContent } from '@types'; diff --git a/src/components/peer-comment/PeerCommentBlock.tsx b/src/components/peer-comment/PeerCommentBlock.tsx index 51b039c..2268b15 100644 --- a/src/components/peer-comment/PeerCommentBlock.tsx +++ b/src/components/peer-comment/PeerCommentBlock.tsx @@ -1,6 +1,5 @@ import { IconTrash } from '@sopt-makers/icons'; -import type { PeerCommentRowState } from '@types'; -import type { PeerCommentStepContent } from './PeerCommentStepTemplate'; +import type { PeerCommentRowState, PeerCommentStepContent } from '@types'; import type { PeerMember } from '@types'; import PeerCommentRecipientBlock from './PeerCommentRecipientBlock'; import InputField from '../common/form/InputField'; diff --git a/src/components/peer-comment/PeerCommentRepeater.tsx b/src/components/peer-comment/PeerCommentRepeater.tsx index ab2ebae..0a5b18d 100644 --- a/src/components/peer-comment/PeerCommentRepeater.tsx +++ b/src/components/peer-comment/PeerCommentRepeater.tsx @@ -1,7 +1,6 @@ import { Button } from '@sopt-makers/ui'; import { IconPlus } from '@sopt-makers/icons'; -import type { PeerCommentRowState } from '@types'; -import type { PeerCommentStepContent } from './PeerCommentStepTemplate'; +import type { PeerCommentRowState, PeerCommentStepContent } from '@types'; import { createEmptyPeerCommentRow } from '@utils/peerCommentUtils'; import PeerCommentBlock from './PeerCommentBlock'; import type { PeerMember } from '@types'; @@ -14,8 +13,12 @@ interface PeerCommentRepeaterProps { peerMembers: PeerMember[]; } -function PeerCommentRepeater({ content, rows, onRowsChange, peerMembers }: PeerCommentRepeaterProps) { - +function PeerCommentRepeater({ + content, + rows, + onRowsChange, + peerMembers, +}: PeerCommentRepeaterProps) { const updateRow = (index: number, next: PeerCommentRowState) => { onRowsChange(rows.map((r, i) => (i === index ? next : r))); }; @@ -30,6 +33,10 @@ function PeerCommentRepeater({ content, rows, onRowsChange, peerMembers }: PeerC onRowsChange([{ id: only.id, memberIds: [], text: '' }]); }; + const isAddDisabled = rows.some( + (row) => row.memberIds.length === 0 || row.text.trim().length === 0, + ); + return (
@@ -53,6 +60,7 @@ function PeerCommentRepeater({ content, rows, onRowsChange, peerMembers }: PeerC size="md" theme="white" LeftIcon={IconPlus} + disabled={isAddDisabled} onClick={() => onRowsChange([...rows, createEmptyPeerCommentRow()])} > 추가 diff --git a/src/components/peer-comment/PeerCommentStepTemplate.tsx b/src/components/peer-comment/PeerCommentStepTemplate.tsx index e2b6c78..b7fdcf9 100644 --- a/src/components/peer-comment/PeerCommentStepTemplate.tsx +++ b/src/components/peer-comment/PeerCommentStepTemplate.tsx @@ -4,49 +4,27 @@ import ContentHeading from '../common/ui/ContentHeading'; import ImageSection from '../common/ui/ImageSection'; import StepLayout from '../common/layout/StepLayout'; import PeerCommentRepeater from './PeerCommentRepeater'; -import { useCommentForm } from '@hooks'; -import { usePeerMembers } from '@hooks'; +import { useCommentForm, usePeerMembers } from '@hooks'; import { createEmptyPeerCommentRow, expandPeerRowsToComments, - hasAtLeastOneCompletePeerRow, isPeerRowValid, } from '@utils/peerCommentUtils'; -import type { Comment, PeerCommentRowState, CommentsKey } from '@types'; +import type { Comment, PeerCommentRowState, CommentsKey, PeerCommentStepContent } from '@types'; import * as styles from './PeerCommentStepTemplate.css'; -export interface PeerCommentStepContent { - commentKey: CommentsKey; - title: string; - description: string; - /** 설명·예시 등 안내 이미지 2장 (순서대로 세로 배치). */ - guideImages?: readonly [string, string]; - /** 블록 상단 제목 (예: Stop Comment를 전달하고 싶은 동료) */ - sectionTitle: string; - questionLabel: string; - textPlaceholder: string; -} - -export interface PeerCommentStepTemplateProps { +interface PeerCommentStepTemplateProps { content: PeerCommentStepContent; currentStep: number; totalSteps?: number; nextPath: string; } -function submissionPatch(commentsKey: CommentsKey, comments: Comment[]) { - switch (commentsKey) { - case 'stop_comments': - return { stop_comments: comments }; - case 'continue_comments': - return { continue_comments: comments }; - case 'start_comments': - return { start_comments: comments }; - default: { - const _exhaustive: never = commentsKey; - return _exhaustive; - } - } +function submissionPatch( + commentsKey: K, + comments: Comment[], +): Record { + return { [commentsKey]: comments } as Record; } function PeerCommentStepTemplate({ @@ -61,7 +39,7 @@ function PeerCommentStepTemplate({ const peerMembers = usePeerMembers(); const [rows, setRows] = useState(() => [createEmptyPeerCommentRow()]); - const isNextEnabled = rows.every(isPeerRowValid) && hasAtLeastOneCompletePeerRow(rows); + const isNextEnabled = rows.every(isPeerRowValid); const handleNext = useCallback(() => { if (!isNextEnabled) { @@ -87,11 +65,16 @@ function PeerCommentStepTemplate({
{guideImages ? ( - - + + ) : null} - +
); diff --git a/src/components/peer-comment/PeerMemberPicker.css.ts b/src/components/peer-comment/PeerMemberPicker.css.ts index 00e9ef7..857ccd1 100644 --- a/src/components/peer-comment/PeerMemberPicker.css.ts +++ b/src/components/peer-comment/PeerMemberPicker.css.ts @@ -23,18 +23,18 @@ export const pickerTriggerButton = style({ }); export const sheetDialogSurface = style({ - margin: 0, - minWidth: 0, + width: '100%', + overflow: 'hidden', +}); + +globalStyle(`body > div:has([data-peer-sheet])`, { width: 'min(386px, calc(100vw - 44px))', maxWidth: 'min(386px, calc(100vw - 44px))', - position: 'fixed', - left: '50%', - bottom: 46, - top: 'auto', - transform: 'translateX(-50%)', + margin: '0 auto', padding: '12px 0 12px', borderRadius: 16, - overflow: 'hidden', + left: '50%', + transform: 'translateX(-50%)', backgroundColor: colors.gray900, }); diff --git a/src/components/peer-comment/PeerMemberPicker.tsx b/src/components/peer-comment/PeerMemberPicker.tsx index 4a93bef..a8ee79d 100644 --- a/src/components/peer-comment/PeerMemberPicker.tsx +++ b/src/components/peer-comment/PeerMemberPicker.tsx @@ -1,5 +1,5 @@ -import { useId, useState } from 'react'; -import { Button, Dialog } from '@sopt-makers/ui'; +import { useState } from 'react'; +import { BottomSheetContent, BottomSheetRoot, Button } from '@sopt-makers/ui'; import { IconCheck, IconChevronLeft, IconUser } from '@sopt-makers/icons'; import * as styles from './PeerMemberPicker.css'; import MemberChip from '../common/ui/MemberChip'; @@ -26,14 +26,11 @@ function PeerMemberPicker({ showRemoveButton = true, }: PeerMemberPickerProps) { const [open, setOpen] = useState(false); - const titleId = useId(); const selectedSet = new Set(memberIds); const peerOptions: PeerOption[] = peerMembers.map((m) => ({ label: m.name, value: m.userId })); const labelById = new Map(peerOptions.map((o) => [o.value, o.label])); - const dialogLabelProps = { - 'aria-labelledby': titleId, - } as const; + const isConfirmDisabled = memberIds.length === 0; return (
@@ -48,71 +45,70 @@ function PeerMemberPicker({ > 누구에게 전달할까? - setOpen(false)} - {...dialogLabelProps} - > -
-
- - 멤버 선택 -
-
- {peerOptions.length === 0 ? ( -

선택할 수 있는 멤버가 없어요.

- ) : ( - peerOptions.map((o) => { - const isSelected = selectedSet.has(o.value); - return ( - + 멤버 선택 +
+
+ {peerOptions.length === 0 ? ( +

선택할 수 있는 멤버가 없어요.

+ ) : ( + peerOptions.map((o) => { + const isSelected = selectedSet.has(o.value); + return ( + - ); - }) - )} -
-
- + {o.label} + {isSelected ? ( + + + + ) : null} + + ); + }) + )} +
+
+ +
-
- + + {memberIds.length > 0 ? (
    {memberIds.map((id) => ( diff --git a/src/constant/peerCommentStepContents.ts b/src/constant/peerCommentStepContents.ts new file mode 100644 index 0000000..011c0a2 --- /dev/null +++ b/src/constant/peerCommentStepContents.ts @@ -0,0 +1,37 @@ +import stopCommentExample from '@assets/stop_comment_example.svg'; +import stopCommentExplanation from '@assets/stop_comment_explanation.svg'; +import startCommentExample from '@assets/start_comment_example.svg'; +import startCommentExplanation from '@assets/start_comment_explanation.svg'; +import continueCommentExample from '@assets/continue_comment_example.svg'; +import continueCommentExplanation from '@assets/continue_comment_explanation.svg'; +import type { PeerCommentStepContent } from '@types'; + +export const STOP_COMMENT_STEP_CONTENT: PeerCommentStepContent = { + commentKey: 'stop_comments', + title: 'Stop 코멘트 작성', + description: '이번 스프린트에서 협업하며 그만했으면 하는 점에 대해 전달해요.', + guideImages: [stopCommentExplanation, stopCommentExample], + sectionTitle: 'Stop Comment를 전달하고 싶은 동료', + questionLabel: '해당 동료가 그만했으면 하는 점은 무엇인가요?', + textPlaceholder: '그만했으면 하는 점', +}; + +export const START_COMMENT_STEP_CONTENT: PeerCommentStepContent = { + commentKey: 'start_comments', + title: 'Start 코멘트 작성', + description: '이번 스프린트에서 협업하며 시작했으면 하는 점에 대해 전달해요.', + guideImages: [startCommentExplanation, startCommentExample], + sectionTitle: 'Start Comment를 전달하고 싶은 동료', + questionLabel: '해당 동료가 시작했으면 하는 점은 무엇인가요?', + textPlaceholder: '시작했으면 하는 점', +}; + +export const CONTINUE_COMMENT_STEP_CONTENT: PeerCommentStepContent = { + commentKey: 'continue_comments', + title: 'Continue 코멘트 작성', + description: '이번 스프린트에서 협업하며 계속했으면 하는 점에 대해 전달해요.', + guideImages: [continueCommentExplanation, continueCommentExample], + sectionTitle: 'Continue Comment를 전달하고 싶은 동료', + questionLabel: '해당 동료가 계속했으면 하는 점은 무엇인가요?', + textPlaceholder: '계속했으면 하는 점', +}; diff --git a/src/pages/ContinueCommentPage.tsx b/src/pages/ContinueCommentPage.tsx new file mode 100644 index 0000000..84256d4 --- /dev/null +++ b/src/pages/ContinueCommentPage.tsx @@ -0,0 +1,14 @@ +import { PeerCommentStepTemplate } from '@components'; +import { CONTINUE_COMMENT_STEP_CONTENT } from '../constant/peerCommentStepContents'; + +function ContinueCommentPage() { + return ( + + ); +} + +export default ContinueCommentPage; diff --git a/src/pages/StartCommentPage.tsx b/src/pages/StartCommentPage.tsx new file mode 100644 index 0000000..1020057 --- /dev/null +++ b/src/pages/StartCommentPage.tsx @@ -0,0 +1,14 @@ +import { PeerCommentStepTemplate } from '@components'; +import { START_COMMENT_STEP_CONTENT } from '../constant/peerCommentStepContents'; + +function StartCommentPage() { + return ( + + ); +} + +export default StartCommentPage; diff --git a/src/pages/StopCommentPage.tsx b/src/pages/StopCommentPage.tsx index 78323b1..052cc33 100644 --- a/src/pages/StopCommentPage.tsx +++ b/src/pages/StopCommentPage.tsx @@ -1,21 +1,12 @@ -import stopCommentExampleImg from '@assets/stop_comment_example.svg'; -import stopCommentExplanationImg from '@assets/stop_comment_explanation.svg'; import { PeerCommentStepTemplate } from '@components'; +import { STOP_COMMENT_STEP_CONTENT } from '../constant/peerCommentStepContents'; function StopCommentPage() { return ( ); } diff --git a/src/types/comment.ts b/src/types/comment.ts index 7e252e2..c36a307 100644 --- a/src/types/comment.ts +++ b/src/types/comment.ts @@ -22,7 +22,6 @@ export type CommentSubmissionPayload = Omit & { mvp: Mv export type CommentsKey = Extract; - export interface CommentSubmitResult { success: boolean; code: 'SUCCESS' | 'INVALID_SPRINT' | 'USER_NOT_FOUND' | 'UNKNOWN_ERROR'; @@ -31,6 +30,18 @@ export interface CommentSubmitResult { export type PeerCommentKind = 'stop' | 'continue' | 'start'; +export interface PeerCommentStepContent { + commentKey: CommentsKey; + title: string; + description: string; + /** 설명·예시 등 안내 이미지 2장 (순서대로 세로 배치). */ + guideImages?: readonly [string, string]; + /** 블록 상단 제목 (예: Stop Comment를 전달하고 싶은 동료) */ + sectionTitle: string; + questionLabel: string; + textPlaceholder: string; +} + export interface PeerCommentRowState { id: string; memberIds: string[]; diff --git a/src/types/index.ts b/src/types/index.ts index 048e610..c54339d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -7,8 +7,8 @@ export type { CommentsKey, CommentSubmitResult, PeerCommentKind, + PeerCommentStepContent, PeerCommentRowState, } from './comment'; export type { PeerMember } from './peer'; export type { SprintInfo, SprintType } from './sprint'; - diff --git a/src/utils/peerCommentUtils.ts b/src/utils/peerCommentUtils.ts index a502ad7..0286e22 100644 --- a/src/utils/peerCommentUtils.ts +++ b/src/utils/peerCommentUtils.ts @@ -22,10 +22,6 @@ export function isPeerRowValid(row: PeerCommentRowState): boolean { return row.memberIds.length > 0 && row.text.trim() !== ''; } -export function hasAtLeastOneCompletePeerRow(rows: PeerCommentRowState[]): boolean { - return rows.some((row) => row.memberIds.length > 0 && row.text.trim() !== ''); -} - /** 한 행의 동일한 본문을 각 memberId마다 `Comment` 한 건으로 펼칩니다. */ export function expandPeerRowsToComments(rows: PeerCommentRowState[]): Comment[] { const result: Comment[] = [];