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
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,12 @@ export default function RetrospectiveCardButtons({
}
}

// 개인 회고 라우트 결정 (추후 라우트 반영 예정)
// 개인 회고 라우트 결정
const getPersonalRetrospectiveRoute = () => {
// TODO: ROUTES.PERSONAL_RETROSPECTIVE, ROUTES.PERSONAL_RETROSPECTIVE_VIEW 추가 후 활성화
if (personalRetrospectiveWritten) {
// return ROUTES.PERSONAL_RETROSPECTIVE_VIEW(gatheringId, meetingId)
return '#'
return ROUTES.PERSONAL_RETROSPECTIVE_VIEW(gatheringId, meetingId)
}
// return ROUTES.PERSONAL_RETROSPECTIVE(gatheringId, meetingId)
return '#'
return ROUTES.PERSONAL_RETROSPECTIVE(gatheringId, meetingId)
}

return (
Expand Down Expand Up @@ -70,12 +67,7 @@ export default function RetrospectiveCardButtons({
<button
type="button"
className="flex flex-1 items-center gap-base rounded-base bg-white p-large shadow-drop cursor-pointer"
onClick={() => {
// TODO: ROUTES.PERSONAL_RETROSPECTIVE, ROUTES.PERSONAL_RETROSPECTIVE_VIEW 추가 후 실제 navigate 활성화
const route = getPersonalRetrospectiveRoute()
console.log('개인 회고 라우트:', route)
// navigate(route)
}}
onClick={() => navigate(getPersonalRetrospectiveRoute())}
>
<div className="flex gap-base">
<img src={personalRetroIcon} alt="개인 회고" className="shrink-0" />
Expand Down
3 changes: 3 additions & 0 deletions src/features/retrospectives/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export * from './components'
export * from './hooks'
export * from './retrospectives.endpoints'

// Personal
export * from './personal'

// API
export { createSttJob, getSummary, publishSummary, updateSummary } from './retrospectives.api'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { X } from 'lucide-react'

import { Container, Input, Textarea } from '@/shared/ui'

import type { UseChangedThoughtsReturn } from '../hooks/useChangedThoughts'
import type { PersonalRetrospectiveTopic } from '../personalRetrospective.types'

export interface ChangedThoughtsSectionProps {
topics: PersonalRetrospectiveTopic[]
form: UseChangedThoughtsReturn
onClose: () => void
}

/**
* 바뀐 나의 생각 섹션
*
* @description
* 각 토픽별로 핵심 쟁점 요약, 모임 전/후 내 의견을 입력하는 폼 섹션입니다.
* 모임 전 의견은 읽기 전용으로 표시되고, 모임 후 의견은 직접 작성합니다.
* 에러 표시 및 스크롤 처리는 상위 컴포넌트에서 담당합니다.
*
* @example
* ```tsx
* <ChangedThoughtsSection topics={topics} form={changedThoughtsForm} onClose={handleClose} />
* ```
*/
export default function ChangedThoughtsSection({
topics,
form,
onClose,
}: ChangedThoughtsSectionProps) {
const { formValues, updateField, getPreOpinion } = form

return (
<section>
<div className="flex flex-col">
<Container className="gap-0">
<div className="flex justify-between items-center mb-large">
<h3 className="text-black typo-heading3">바뀐 나의 생각</h3>
<button
type="button"
onClick={onClose}
className="text-grey-400 hover:text-black transition-colors"
aria-label="바뀐 나의 생각 섹션 닫기"
>
<X className="size-6 text-grey-600 cursor-pointer" />
</button>
</div>
<div className="flex flex-col gap-xlarge">
{formValues.map((item) => {
return (
<div key={item.topicId}>
<p className="text-black typo-subtitle3 mb-small">
{topics.find((t) => t.topicId === item.topicId)?.topicName}
</p>

<Input
placeholder="핵심 쟁점을 요약해주세요"
value={item.coreSummary}
onChange={(e) => updateField(item.topicId, 'coreSummary', e.target.value)}
className="mb-medium"
/>

{getPreOpinion(item.topicId) ? (
<div className="grid grid-cols-2 gap-base">
<div className="flex flex-col gap-tiny">
<span className="text-grey-600 typo-body4">모임 전 내 의견</span>
<Textarea
value={getPreOpinion(item.topicId)}
disabled
height={160}
className="bg-grey-200 text-black"
/>
</div>
<div className="flex flex-col gap-tiny">
<span className="text-grey-600 typo-body4">모임 후 내 의견</span>
<Textarea
placeholder="감상문을 입력해주세요"
value={item.postOpinion}
onChange={(e) => updateField(item.topicId, 'postOpinion', e.target.value)}
height={160}
/>
</div>
</div>
) : (
<div className="flex flex-col gap-xsmall">
<span className="text-grey-600 typo-body4">변화된 나의 생각</span>
<Textarea
placeholder="감상문을 입력해주세요"
value={item.postOpinion}
onChange={(e) => updateField(item.topicId, 'postOpinion', e.target.value)}
height={104}
/>
</div>
)}
</div>
)
})}
</div>
</Container>
</div>
</section>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Trash2, X } from 'lucide-react'

import { Button, Container, Input, Textarea } from '@/shared/ui'

import type { UseFreeRecordReturn } from '../hooks/useFreeRecord'

export interface FreeRecordSectionProps {
form: UseFreeRecordReturn
showErrors: boolean
onClose: () => void
}

/**
* 자유 기록 섹션
*
* @description
* 자유롭게 기록을 남기는 동적 폼 섹션입니다.
* 제목+상세내용 항목을 동적으로 추가/삭제할 수 있습니다.
*
* @example
* ```tsx
* <FreeRecordSection form={freeRecordForm} showErrors={showErrors} />
* ```
*/
export default function FreeRecordSection({ form, showErrors, onClose }: FreeRecordSectionProps) {
const { entries, addEntry, removeEntry, updateEntry } = form

return (
<section>
<Container className="gap-[19px]">
<div className="flex justify-between items-center">
<h3 className="text-black typo-heading3">자유 기록</h3>
<button
type="button"
onClick={onClose}
className="text-grey-400 hover:text-black transition-colors"
aria-label="자유 기록 섹션 닫기"
>
<X className="size-6 text-grey-600 cursor-pointer" />
</button>
</div>
{entries.map((entry) => {
const isPartial = form.isEntryPartial(entry.id)
const titleError = showErrors && isPartial && entry.title.trim() === ''
const contentError = showErrors && isPartial && entry.content.trim() === ''

return (
<div
key={entry.id}
className="flex flex-col gap-small rounded-small bg-grey-100 border border-grey-300 p-medium"
{...(showErrors && isPartial ? { 'data-field-error': '' } : {})}
>
<div className="flex flex-col gap-tiny">
<span className="text-grey-600 typo-body4">제목</span>
<Input
placeholder="제목을 작성해주세요"
value={entry.title}
onChange={(e) => updateEntry(entry.id, 'title', e.target.value)}
error={titleError}
errorMessage={titleError ? '내용을 입력해주세요' : undefined}
/>
</div>
<div className="flex flex-col gap-tiny">
<span className="text-grey-600 typo-body4">상세 내용</span>
<Textarea
placeholder="내용을 작성해주세요"
value={entry.content}
onChange={(e) => updateEntry(entry.id, 'content', e.target.value)}
height={104}
error={contentError}
errorMessage={contentError ? '내용을 입력해주세요' : undefined}
/>
</div>

<div className="flex justify-end">
<button
type="button"
className="text-grey-400 hover:text-accent-300 transition-colors"
onClick={() => {
removeEntry(entry.id)
if (entries.length === 1) onClose()
}}
aria-label="항목 삭제"
>
<Trash2 className="size-5" />
</button>
</div>
</div>
)
})}
<Button variant="secondary" outline onClick={addEntry}>
+ 항목 추가하기
</Button>
</Container>
</section>
)
}
Loading
Loading