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({
>
누구에게 전달할까?
-