Skip to content
Closed
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
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"prepare": "husky"
},
"dependencies": {
"@openai/codex": "^0.87.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

@openai/codex 패키지가 추가되었는데, 이 프로젝트에서 사용될 예정인가요? 만약 의도치 않게 추가된 의존성이라면, 번들 크기를 줄이고 잠재적인 보안 위험을 피하기 위해 제거하는 것이 좋습니다.

"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.90.16",
"@tanstack/react-query-devtools": "^5.91.2",
Expand Down
23 changes: 19 additions & 4 deletions src/components/comment/CommentInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ interface CommentInputProps {
className?: string;
/** textarea className */
textareaClassName?: string;
/** theme variant */
theme?: 'light' | 'dark';
}

/**
Expand All @@ -51,10 +53,12 @@ export default function CommentInput({
autoFocus = false,
className,
textareaClassName,
theme = 'light',
}: CommentInputProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null);

const isEmpty = !value.trim();
const isDark = theme === 'dark';

/** textarea 높이를 내용에 맞게 자동 조절 */
useEffect(() => {
Expand Down Expand Up @@ -105,16 +109,25 @@ export default function CommentInput({
onKeyDown={handleKeyDown}
aria-label={placeholder}
className={clsx(
'w-full overflow-hidden resize-none bg-transparent border-b border-gray-600 pt-2 pb-2 outline-none placeholder:text-gray-600 focus:border-main transition-colors',
textareaClassName ?? 'text-body-m-bold text-black',
[
'w-full overflow-hidden resize-none bg-transparent border-b pt-2 pb-2 outline-none transition-colors',
isDark
? 'border-gray-400 placeholder:text-gray-400 focus:border-main'
: 'border-gray-600 placeholder:text-gray-600 focus:border-main',
],
textareaClassName ??
(isDark ? 'text-body-m-bold text-white' : 'text-body-m-bold text-black'),
)}
/>

<div className="flex gap-2 items-center justify-end">
<button
type="button"
onClick={handleCancel}
className="px-3 py-1.5 rounded-full text-caption-bold text-gray-800 hover:opacity-80 transition focus-visible:outline-2 focus-visible:outline-main"
className={clsx(
'px-3 py-1.5 rounded-full text-caption-bold hover:opacity-80 transition focus-visible:outline-2 focus-visible:outline-main',
isDark ? 'text-gray-300' : 'text-gray-800',
)}
>
취소
</button>
Expand All @@ -126,7 +139,9 @@ export default function CommentInput({
className={clsx(
'px-3 py-1.5 rounded-full text-caption-bold transition focus-visible:outline-2 focus-visible:outline-main',
isEmpty
? 'bg-white text-gray-400 cursor-not-allowed'
? isDark
? 'bg-white text-gray-400 cursor-not-allowed'
: 'bg-gray-800 text-gray-500 cursor-not-allowed'
: 'bg-main text-white hover:opacity-90',
)}
>
Expand Down
39 changes: 31 additions & 8 deletions src/components/comment/CommentItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ interface CommentItemProps {
onReplySubmit?: (targetId: string) => void;
/** ID로 답글 토글 (재귀용) */
onToggleReplyById?: (targetId: string) => void;
/** theme variant */
theme?: 'light' | 'dark';
}

/**
Expand All @@ -73,11 +75,13 @@ function CommentItem({
setReplyingToId,
onReplySubmit,
onToggleReplyById,
theme = 'light',
}: CommentItemProps) {
const user = MOCK_USERS.find((u) => u.id === comment.authorId);
const authorName = user?.name ?? '알 수 없음';
const authorProfileImage = user?.profileImage;

const isDark = theme === 'dark';
const handleChildToggle = useCallback(
(id: string) => {
onToggleReplyById?.(id);
Expand Down Expand Up @@ -111,7 +115,13 @@ function CommentItem({
className={clsx(
'flex gap-3 py-3 pr-4 transition-colors',
isIndented ? 'pl-15' : 'pl-4',
isActive ? 'bg-gray-200' : 'bg-gray-100',
isDark
? isActive
? 'bg-gray-800'
: 'bg-gray-900'
: isActive
? 'bg-gray-200'
: 'bg-gray-100',
)}
>
<div className="w-8 shrink-0">
Expand All @@ -130,8 +140,15 @@ function CommentItem({
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="max-w-50 truncate text-body-s-bold text-black">{authorName}</span>
<span className="text-caption text-gray-600">
<span
className={clsx(
'max-w-50 truncate text-body-s-bold',
isDark ? 'text-white' : 'text-black',
)}
>
{authorName}
</span>
<span className={clsx('text-caption', isDark ? 'text-gray-400' : 'text-gray-600')}>
{formatRelativeTime(comment.timestamp)}
</span>
</div>
Expand All @@ -149,7 +166,7 @@ function CommentItem({
)}
</div>

<div className="text-body-s text-black">
<div className={clsx('text-body-s', isDark ? 'text-gray-100' : 'text-black')}>
{comment.slideRef && onGoToSlideRef && (
<button
type="button"
Expand All @@ -173,8 +190,12 @@ function CommentItem({
className={clsx(
'flex items-center gap-1 rounded text-caption-bold transition focus-visible:outline-2 focus-visible:outline-main',
isActive
? 'text-gray-400'
: 'text-main hover:text-main-variant1 active:text-main-variant2',
? isDark
? 'text-gray-500'
: 'text-gray-400'
: isDark
? 'text-main-variant1 hover:text-main active:text-main-variant2'
: 'text-main hover:text-main-variant1 active:text-main-variant2',
)}
>
답글
Expand All @@ -191,8 +212,9 @@ function CommentItem({
onSubmit={onSubmitReply}
onCancel={onCancelReply}
autoFocus
className="pb-4 pr-4 pl-15 bg-gray-200"
textareaClassName="text-body-s text-black"
className={clsx('pb-4 pr-4 pl-15', isDark ? 'bg-gray-900' : 'bg-gray-200')}
textareaClassName={clsx('text-body-s', isDark ? 'text-white' : 'text-black')}
theme={theme}
/>
)}

Expand All @@ -216,6 +238,7 @@ function CommentItem({
onReplySubmit={onReplySubmit}
onToggleReplyById={onToggleReplyById}
isIndented={false}
theme={theme}
/>
))}
</div>
Expand Down
5 changes: 4 additions & 1 deletion src/components/comment/CommentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ interface CommentListProps {
onAddReply: (targetId: string, content: string) => void;
onGoToSlideRef: (ref: string) => void;
onDeleteComment?: (commentId: string) => void;
theme?: 'light' | 'dark';
}

export default function CommentList({
comments,
onAddReply,
onGoToSlideRef,
onDeleteComment,
theme = 'light',
}: CommentListProps) {
const [replyingToId, setReplyingToId] = useState<string | null>(null);
const [replyDraft, setReplyDraft] = useState('');
Expand All @@ -46,7 +48,7 @@ export default function CommentList({
};

return (
<div className="mt-2 flex-1 space-y-2 overflow-y-auto">
<div className="mr-10 md:mr-35 mt-2 flex-1 space-y-2 overflow-y-auto">
{comments.map((comment) => (
<CommentItem
key={comment.id}
Expand All @@ -64,6 +66,7 @@ export default function CommentList({
setReplyingToId={setReplyingToId}
onReplySubmit={handleReplySubmit}
onToggleReplyById={handleToggleReply}
theme={theme}
/>
))}
</div>
Expand Down
68 changes: 68 additions & 0 deletions src/components/common/DarkHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { ReactNode } from 'react';

import clsx from 'clsx';

import Informaion from '@/assets/icons/icon-info.svg?react';
import { Logo, Popover } from '@/components/common';

interface DarkHeaderProps {
title: string;
renderRight?: ReactNode;
publisher?: string;
publishedAt?: string;
}

export const DarkHeader = ({
title,
renderRight,
publisher = '익명의 바다거북이',
publishedAt = '2025.11.25 21:10:34',
}: DarkHeaderProps) => {
Comment on lines +11 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

publisherpublishedAt이 선택적 프로퍼티로 정의되어 있고, 컴포넌트 내에서 하드코딩된 기본값을 가집니다. PR 설명에 언급된 대로 이 값들은 동적으로 받아와야 하므로, 필수 프로퍼티로 변경하고 기본값을 제거하여 부모 컴포넌트에서 명시적으로 값을 전달하도록 강제하는 것이 좋습니다.

Suggested change
publisher?: string;
publishedAt?: string;
}
export const DarkHeader = ({
title,
renderRight,
publisher = '익명의 바다거북이',
publishedAt = '2025.11.25 21:10:34',
}: DarkHeaderProps) => {
publisher: string;
publishedAt: string;
}
export const DarkHeader = ({
title,
renderRight,
publisher,
publishedAt,
}: DarkHeaderProps) => {

return (
<header className="flex h-15 w-full shrink-0 items-center justify-between border-b border-gray-600 bg-gray-800 px-6 md:px-18">
<div className="flex items-center gap-4">
<Logo />

<div className="flex items-center gap-1.5">
<span className="text-sm font-bold text-white">{title}</span>

<Popover
trigger={({ isOpen }) => (
<button
type="button"
className={clsx(
'flex items-center justify-center transition-colors',
// 열려있거나 호버시 흰색, 평소엔 회색
isOpen ? 'text-white' : 'text-gray-200 hover:text-white',
)}
>
<Informaion className="h-4 w-4 translate-y-[1px]" />
</button>
)}
align="end" // 팝오버 왼쪽 정렬 (취향에 따라 center로 변경 가능)
position="bottom"
className="w-[250px] max-w-[calc(100vw-2rem)] rounded-lg bg-white p-4 shadow-lg ring-1 ring-black/5 left-1/2 right-auto -translate-x-1/2 md:left-auto md:right-0 md:translate-x-0"
>
{/* 팝오버 내부 콘텐츠 (사진 디자인 반영) */}
<div className="flex flex-col gap-2">
{/* 행 1: 게시자 */}
<div className="flex items-center gap-4">
<span className="w-14 text-body-s-bold text-gray-600">게시자</span>
<span className="text-body-s text-gray-800">{publisher}</span>
</div>

{/* 행 2: 게시 날짜 */}
<div className="flex items-center gap-4">
<span className="w-14 text-body-s-bold text-gray-600">게시 날짜</span>
<span className="text-body-s text-gray-800">{publishedAt}</span>
</div>
</div>
</Popover>
</div>
</div>
<div className="flex items-center">{renderRight}</div>
</header>
);
};

export default DarkHeader;
11 changes: 9 additions & 2 deletions src/components/common/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface LayoutProps {
export function Layout({ left, center, right, theme, scrollable = false, children }: LayoutProps) {
const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
const appliedTheme = theme ?? resolvedTheme;
const isDark = appliedTheme === 'dark';

// 테마가 변경되거나 오버라이드될 때 document.documentElement에 적용 (모달 등 포탈 지원)
useEffect(() => {
Expand All @@ -48,9 +49,15 @@ export function Layout({ left, center, right, theme, scrollable = false, childre
return (
<div
data-theme={appliedTheme}
className={`bg-gray-100 ${scrollable ? 'min-h-screen' : 'h-screen overflow-hidden'}`}
className={`${isDark ? 'bg-gray-900' : 'bg-gray-100'} ${scrollable ? 'min-h-screen' : 'h-screen overflow-hidden'}`}
>
<header className="fixed top-0 right-0 left-0 z-50 flex h-15 items-center justify-between border-b border-gray-200 bg-white px-18">
<header
className={`fixed top-0 right-0 left-0 z-50 flex h-15 items-center justify-between border-b px-18 ${
isDark
? 'border-gray-800 bg-gray-900 text-gray-100'
: 'border-gray-200 bg-white text-gray-900'
}`}
>
<div className="flex items-center gap-6">{left ?? <Logo />}</div>
<div className="absolute left-1/2 -translate-x-1/2">{center}</div>
<div className="flex items-center gap-8">{right}</div>
Expand Down
Loading
Loading