diff --git a/package-lock.json b/package-lock.json index 347bc2c2..578492a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "ttorang-frontend", "version": "0.0.0", "dependencies": { + "@openai/codex": "^0.87.0", "@tailwindcss/vite": "^4.1.18", "@tanstack/react-query": "^5.90.16", "@tanstack/react-query-devtools": "^5.91.2", @@ -1560,6 +1561,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@openai/codex": { + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.87.0.tgz", + "integrity": "sha512-VaPB2gcJ6XLqs8Kvv5bkel1cItSDZIHk0xA71IFux+lYTHmyV6ihup6FolSWROmiYfr7dXxPrtb01CQ7sl0AdA==", + "license": "Apache-2.0", + "bin": { + "codex": "bin/codex.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.47", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", diff --git a/package.json b/package.json index 706dc941..9a6845ca 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "prepare": "husky" }, "dependencies": { + "@openai/codex": "^0.87.0", "@tailwindcss/vite": "^4.1.18", "@tanstack/react-query": "^5.90.16", "@tanstack/react-query-devtools": "^5.91.2", diff --git a/src/components/comment/CommentInput.tsx b/src/components/comment/CommentInput.tsx index 61838f83..51f474b5 100644 --- a/src/components/comment/CommentInput.tsx +++ b/src/components/comment/CommentInput.tsx @@ -28,6 +28,8 @@ interface CommentInputProps { className?: string; /** textarea className */ textareaClassName?: string; + /** theme variant */ + theme?: 'light' | 'dark'; } /** @@ -51,10 +53,12 @@ export default function CommentInput({ autoFocus = false, className, textareaClassName, + theme = 'light', }: CommentInputProps) { const textareaRef = useRef(null); const isEmpty = !value.trim(); + const isDark = theme === 'dark'; /** textarea 높이를 내용에 맞게 자동 조절 */ useEffect(() => { @@ -105,8 +109,14 @@ 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'), )} /> @@ -114,7 +124,10 @@ export default function CommentInput({ @@ -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', )} > diff --git a/src/components/comment/CommentItem.tsx b/src/components/comment/CommentItem.tsx index fb2a8194..61e2a1f8 100644 --- a/src/components/comment/CommentItem.tsx +++ b/src/components/comment/CommentItem.tsx @@ -49,6 +49,8 @@ interface CommentItemProps { onReplySubmit?: (targetId: string) => void; /** ID로 답글 토글 (재귀용) */ onToggleReplyById?: (targetId: string) => void; + /** theme variant */ + theme?: 'light' | 'dark'; } /** @@ -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); @@ -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', )} >
@@ -130,8 +140,15 @@ function CommentItem({
- {authorName} - + + {authorName} + + {formatRelativeTime(comment.timestamp)}
@@ -149,7 +166,7 @@ function CommentItem({ )}
-
+
{comment.slideRef && onGoToSlideRef && (
diff --git a/src/components/comment/CommentList.tsx b/src/components/comment/CommentList.tsx index 7a0ae2ce..01d7d168 100644 --- a/src/components/comment/CommentList.tsx +++ b/src/components/comment/CommentList.tsx @@ -16,6 +16,7 @@ interface CommentListProps { onAddReply: (targetId: string, content: string) => void; onGoToSlideRef: (ref: string) => void; onDeleteComment?: (commentId: string) => void; + theme?: 'light' | 'dark'; } export default function CommentList({ @@ -23,6 +24,7 @@ export default function CommentList({ onAddReply, onGoToSlideRef, onDeleteComment, + theme = 'light', }: CommentListProps) { const [replyingToId, setReplyingToId] = useState(null); const [replyDraft, setReplyDraft] = useState(''); @@ -46,7 +48,7 @@ export default function CommentList({ }; return ( -
+
{comments.map((comment) => ( ))}
diff --git a/src/components/common/DarkHeader.tsx b/src/components/common/DarkHeader.tsx new file mode 100644 index 00000000..3d4cfde5 --- /dev/null +++ b/src/components/common/DarkHeader.tsx @@ -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) => { + return ( +
+
+ + +
+ {title} + + ( + + )} + 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" + > + {/* 팝오버 내부 콘텐츠 (사진 디자인 반영) */} +
+ {/* 행 1: 게시자 */} +
+ 게시자 + {publisher} +
+ + {/* 행 2: 게시 날짜 */} +
+ 게시 날짜 + {publishedAt} +
+
+
+
+
+
{renderRight}
+
+ ); +}; + +export default DarkHeader; diff --git a/src/components/common/layout/Layout.tsx b/src/components/common/layout/Layout.tsx index 570f8132..d047e1d3 100644 --- a/src/components/common/layout/Layout.tsx +++ b/src/components/common/layout/Layout.tsx @@ -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(() => { @@ -48,9 +49,15 @@ export function Layout({ left, center, right, theme, scrollable = false, childre return (
-
+
{left ?? }
{center}
{right}
diff --git a/src/components/feedback/FeedbackInput.tsx b/src/components/feedback/FeedbackInput.tsx new file mode 100644 index 00000000..583092bd --- /dev/null +++ b/src/components/feedback/FeedbackInput.tsx @@ -0,0 +1,141 @@ +// 우측 하단 입력 및 리액션 바 +// components/feedback/FeedbackInput.tsx +import { useRef, useState } from 'react'; + +import { type EmojiReaction, REACTION_CONFIG, type ReactionType } from '@/types/script'; + +interface Props { + reactions: EmojiReaction[]; + onToggleReaction: (emoji: ReactionType) => void; + onAddComment: (content: string) => void; + // 'all'(기존), 'reactions-only'(중간바), 'input-only'(댓글탭하단) + viewType?: 'all' | 'reactions-only' | 'input-only'; +} + +export default function FeedbackInput({ + reactions, + onToggleReaction, + onAddComment, + viewType = 'all', +}: Props) { + const [commentDraft, setCommentDraft] = useState(''); + const commentTextareaRef = useRef(null); + + const showInput = viewType === 'all' || viewType === 'input-only'; + const showReactions = viewType === 'all' || viewType === 'reactions-only'; + + const handleAddComment = () => { + onAddComment(commentDraft); + setCommentDraft(''); + if (commentTextareaRef.current) { + commentTextareaRef.current.style.height = 'auto'; + } + }; + + const handleCancel = () => { + setCommentDraft(''); + if (commentTextareaRef.current) { + commentTextareaRef.current.style.height = 'auto'; + } + }; + + const formatReactionCount = (count: number) => (count > 99 ? '99+' : count); + + const isMobileReactionsOnly = viewType === 'reactions-only'; + + return ( +
+ {/* 댓글 입력 영역 */} + {showInput && ( +
+