diff --git a/src/App.tsx b/src/App.tsx index b7e4427..f510eff 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,9 @@ import './App.css' +import { AnnoymousNickNameProvider } from './AuthorData'; import { CommentInput, CommentItem, CommentListContainer, CommentPagination, PostListControls, SubCommentData } from './Comment'; import CommentHeader from './CommentHeader'; import { Footer } from './Footer'; -import { GalleryContent } from './Gallery'; +import { GalleryContent, GalleryContentHeader } from './Gallery'; import { GalleryTitleHeader } from './GalleryTitleHeader'; import { GlobalNavigationBar, Header, VisitHistory } from './Header'; import { LoginBox } from './Sidebar'; @@ -106,7 +107,6 @@ const tableData: TableRowData[] = [ views: "", recommendations: "", isNews: true, - titleLinkUrl: '#', // Add actual URL }, ]; @@ -164,66 +164,105 @@ const comments: SubCommentData[] = [ function App() { return ( - <div> - <Header /> - <GlobalNavigationBar /> - <VisitHistory /> - <div className='relative w-[1450px] mx-auto'> - <main - className='w-[1160px] m-[20px_auto_0]' - > - <section className='' > - <GalleryTitleHeader /> - <div - className='border-custom-blue-dark border w-[1158px]' - /> - <GalleryContent /> - <CommentHeader /> - <CommentListContainer> - <CommentItem comment={{ - id: 1, - author: { type: "nickname", nickname: "동아리망했다" }, - text: '너 지금 상현이햄을 ■■라고', - timestamp: '03.31 17:44:25', - showDelete: true, - }} /> - <CommentItem comment={{ - id: 2, - author: { type: "IP", ip: "218.144" }, - text: '그냥 미국인인데 조센징들은 왜 조선계라고 못 넣어서 안달일까', - timestamp: '03.31 18:16:45', - showDelete: true, - subComments: comments, - }} /> - <CommentItem comment={{ - id: 3, - author: { type: "IP", ip: "123.245" }, - text: 'aaa', - timestamp: '03.31 18:16:45', - showDelete: true, - }} /> - </CommentListContainer> - <CommentPagination currentPage={1} maxPage={2} /> - <CommentInput /> - <PostListControls owner /> - <div className='flex justify-between'> - <div style={{ - width: "840px", - }}> - <GalleryTable data={tableData} /> - <PostListControls /> + <AnnoymousNickNameProvider value="썬갤러"> + <div> + <Header /> + <GlobalNavigationBar /> + <VisitHistory recentVisits={[ + { + id: 1, + name: "워썬더", + isMinor: true, + }, + { + id: 2, + name: "배틀그라운드", + isMinor: false, + }, + { + id: 3, + name: "리그오브레전드", + isMinor: false, + }, + { + id: 4, + name: "발로란트", + isMinor: false, + }, + { + id: 5, + name: "오버워치", + isMinor: true, + } + ]} /> + <div className='relative w-[1450px] mx-auto'> + <main + className='w-[1160px] m-[20px_auto_0]' + > + <section className='' > + <GalleryTitleHeader title='워썬더 갤러리' /> + <div + className='border-custom-blue-dark border w-[1158px]' + /> + <GalleryContentHeader kind='일반' + title={'아 e글 유파들이 고도 왜 안올리는지 알았다'} + author={{ + type: "IP", + ip: "183.96", + }} + date='2025.02.04 21:32:47' + views={1234} + recommendations={12} + commentCount={9} + /> + <GalleryContent /> + <CommentHeader commentCount={9} /> + <CommentListContainer> + <CommentItem comment={{ + id: 1, + author: { type: "nickname", nickname: "동아리망했다" }, + text: '너 지금 상현이햄을 ■■라고', + timestamp: '03.31 17:44:25', + showDelete: true, + }} /> + <CommentItem comment={{ + id: 2, + author: { type: "IP", ip: "218.144" }, + text: '그냥 미국인인데 조센징들은 왜 조선계라고 못 넣어서 안달일까', + timestamp: '03.31 18:16:45', + showDelete: true, + subComments: comments, + }} /> + <CommentItem comment={{ + id: 3, + author: { type: "IP", ip: "123.245" }, + text: 'aaa', + timestamp: '03.31 18:16:45', + showDelete: true, + }} /> + </CommentListContainer> + <CommentPagination currentPage={1} maxPage={2} /> + <CommentInput /> + <PostListControls owner /> + <div className='flex justify-between'> + <div style={{ + width: "840px", + }}> + <GalleryTable data={tableData} /> + <PostListControls /> + </div> + <div style={{ + width: "300px", + }}> + <LoginBox /> + </div> </div> - <div style={{ - width: "300px", - }}> - <LoginBox /> - </div> - </div> - </section> - </main> - </div> - <Footer /> - </div> + </section> + </main> + </div> + <Footer /> + </div > + </AnnoymousNickNameProvider> ) } diff --git a/src/AuthorData.tsx b/src/AuthorData.tsx new file mode 100644 index 0000000..8f13fd8 --- /dev/null +++ b/src/AuthorData.tsx @@ -0,0 +1,89 @@ +import { createContext, useContext, useMemo } from "react"; + +export type AuthorData = { + // 운영자 + type: "operator"; +} | { + // 고닉 + // 중복 불가능 닉네임 + type: "nickname"; + nickname: string; + // 파딱, 주딱 구분을 위한 userType + userType?: "manager" | "submanager"; // Optional, if applicable +} | { + // 유동닉 + type: "IP"; + // IP 주소 + ip: string; + + tempNickname?: string; // Optional, if applicable +} | { + // 반유동닉 + // 중복 가능 닉네임 + type: "semi-nickname"; + nickname: string; +}; + +const NicknameImagePath = { + "주딱": "/fix_managernik.gif", + "파딱": "/fix_sub_managernik.gif", + "반유동": "/nik.gif", + "default": "/fix_nik.gif" +} + +const AnnoymousNickName = createContext("ㅇㅇ"); +export const AnnoymousNickNameProvider = AnnoymousNickName.Provider; + +export function NickName({ author } : { + author: AuthorData, +}) { + const defaultNickname = useContext(AnnoymousNickName); + + const nickname = useMemo(() => { + switch (author.type) { + case "nickname": + return author.nickname; + case "semi-nickname": + return author.nickname; + case "IP": + return author.tempNickname ?? defaultNickname; + case "operator": + return "운영자"; + default: + return defaultNickname; + } + }, [author, defaultNickname]) + + return <> + {/* Author Name Span */} + <span className="inline-block max-w-[81%] align-top text-ellipsis overflow-hidden whitespace-nowrap"> + {/* Inner em/span for potential finer control if needed */} + <em className="not-italic leading-[13px]"> + {nickname} + </em> + </span> + {/* Author IP */} + {author?.type === "IP" && ( + <span className="font-tahoma text-[11px] text-custom-gray-medium tracking-[-0.05em] ml-[3px]"> + ({author.ip}) + </span> + )} + {/* Author Icon Placeholder */} + {author?.type !== "IP" && ( + <a className="text-custom-gray-dark ml-0.5 inline-block"> + {/* Replace with actual icon component or img tag */} + <img + alt="icon" + src={ + author?.type === "nickname" ? ( + author?.userType === "manager" ? NicknameImagePath["주딱"] : + author?.userType === "submanager" ? NicknameImagePath["파딱"] : + NicknameImagePath["default"] + ) : NicknameImagePath["반유동"] + } + className="align-middle cursor-pointer w-3 h-3" // Example size + /> + </a> + )} + </> +} \ No newline at end of file diff --git a/src/Comment.tsx b/src/Comment.tsx index 451deb0..09fa766 100644 --- a/src/Comment.tsx +++ b/src/Comment.tsx @@ -1,5 +1,5 @@ import { Separator } from "./Separator"; -import { AuthorData } from "./table"; +import { AuthorData } from './AuthorData'; import { cn } from "./util/cn"; import React, { useState } from 'react'; diff --git a/src/Gallery.tsx b/src/Gallery.tsx index 1608dff..96dd201 100644 --- a/src/Gallery.tsx +++ b/src/Gallery.tsx @@ -1,15 +1,37 @@ import { Separator } from "./Separator" +import { AuthorData, NickName } from './AuthorData'; -function GalleryContentHeader() { +export interface GalleryContentHeaderProps { + title: string; + kind: string; + author: AuthorData; + date: string; + views?: number; + recommendations?: number; + /** + * Number of comments on the post + */ + commentCount?: number; +} + +export function GalleryContentHeader({ + title, + kind, + author, + date, + views = 0, + recommendations = 0, + commentCount = 0, +}: GalleryContentHeaderProps) { return ( <header> {/* Outer container with margin, padding, and bottom border */} <div className="mt-4 mb-[29px] pb-[11px] border-b border-solid border-gray-200"> {/* Post Title */} <h3 className="px-0.5 mb-[7px] text-sm font-bold"> - <span> [일반] </span> + <span> [{kind}] </span> <span> - 아 e글 유파들이 고도 왜 안올리는지 알았다 + {title} </span> </h3> @@ -19,16 +41,10 @@ function GalleryContentHeader() { > {/* Left section: Author, IP, Timestamp */} <div> - <span> - {/* author name */} - <em className="not-italic"> 썬갤러 </em> {/* Adjusted em styling for clarity */} - </span> - <span className="font-tahoma text-[11px] text-gray-500 ml-1"> - (110.15) - </span> + <NickName author={author} /> <span className="cursor-default"> <Separator /> - 2025.02.04 21:32:47 + {date} </span> </div> @@ -37,11 +53,11 @@ function GalleryContentHeader() { className="pr-[7px]" > <span className="cursor-default"> - 조회 65 + 조회 {views} </span> <Separator /> <span className="cursor-default"> - 추천 0 + 추천 {recommendations} </span> <Separator /> <span className="cursor-default"> @@ -50,7 +66,7 @@ function GalleryContentHeader() { className="inline-block h-5 leading-5 px-2.5 bg-gray-200 text-gray-700 border border-gray-300 rounded-full hover:bg-gray-300 hover:border-gray-400 text-xs"> - 댓글 13 + 댓글 {commentCount} </a> </span> </div> @@ -76,16 +92,7 @@ function GalleryRecommendation({ pt-[19px] w-fit box-content"> <div className="flex items-center justify-center overflow-hidden pb-2"> <div className="flex justify-end overflow-hidden w-[139px] mb-0.5"> - <div - style={{ - width: "67px", - paddingTop: "10px", - paddingLeft: "11px", - textAlign: "center", - fontWeight: "bold", - color: "rgb(85, 85, 85)" - }} - > + <div className="text-custom-dropdown-text text-center pt-[10px] pl-[11px] w-[67px] font-bold"> <p className="text-base leading-[22px] text-[#d31900] font-bold" >{recommendCount}</p> <p @@ -108,16 +115,12 @@ function GalleryRecommendation({ > <em style={{ - backgroundColor: "transparent", - backgroundImage: - 'url("/sp_image.png")', - backgroundRepeat: "no-repeat", backgroundPositionX: "0px", backgroundPositionY: "-315px", - display: "inline-block", width: "56px", height: "56px", }} + className="bg-sp-img inline-block" ></em> </button> </div> @@ -276,23 +279,18 @@ export function GalleryContent() { return <article className="text-custom-gray-dark text-[13px] font-apple" > - <GalleryContentHeader /> <div style={{ lineHeight: "22px" }}> <div style={{ marginBottom: "50px" }}> - <div - style={{ - overflow: "hidden", - position: "relative" - }} - > + <div className="overflow-hidden relative"> <div> - <span style={{ marginLeft: "10px" }}> + <span className="ml-[10px]"> <img style={{ maxWidth: "100%", width: "550px", height: "350px", }} + alt="alt" /> </span> </div> diff --git a/src/GalleryTitleHeader.tsx b/src/GalleryTitleHeader.tsx index efcbab9..3b7dcbe 100644 --- a/src/GalleryTitleHeader.tsx +++ b/src/GalleryTitleHeader.tsx @@ -1,13 +1,24 @@ import { Separator } from "./Separator" -export function GalleryTitleHeader() { +interface GalleryTitleHeaderProps { + title?: string; + relatedGalleriesCount?: { + current: number; + total: number; + }; +} + +export function GalleryTitleHeader({ + title = "워썬더 갤러리", + relatedGalleriesCount = { current: 2, total: 8 }, +}: GalleryTitleHeaderProps = {}) { return ( <header className="bg-transparent h-[37px] mb-[3px] pt-[4px] text-gray-500"> <div className="flex justify-between items-center"> <div className="flex items-center"> <h2 className="mt-[-2px] mr-[6px] ml-[2px] text-[24px] max-w-[420px] font-[nanumGothic] tracking-[-1px] m-[2px_8px_0_3px] overflow-hidden whitespace-nowrap text-ellipsis text-custom-blue-dark"> <a className="text-custom-blue-dark font-bold"> - 워썬더 갤러리 + {title} <div className="bg-sp-img bg-no-repeat inline-block align-top ml-[4px] w-[22px] h-[22px] bg-[-195px_-844px] mt-[3px]"> </div> </a> @@ -26,14 +37,20 @@ export function GalleryTitleHeader() { </div> <Separator /> <button className="cursor-pointer align-top font-sans"> - 연관 갤러리(2/8) + 연관 갤러리({relatedGalleriesCount.current}/{relatedGalleriesCount.total}) <span className="mr-[2px] ml-[2px] hidden"> </span> <em className="bg-sp-img bg-no-repeat inline-block w-[9px] h-[5px] bg-[-115px_-43px] align-[1px] ml-[2px]"> </em> </button> <Separator /> - <button className="cursor-pointer align-top"> + <button className="cursor-pointer align-top" + onClick={() => { + prompt("해당 글의 주소입니다.\nCtrl+C를 눌러 클립보드로 복사하세요.", + window.location.href + ) + }} + > 갤주소 복사 </button> <Separator /> diff --git a/src/Header.tsx b/src/Header.tsx index cc73499..bedd4ae 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -375,17 +375,20 @@ export function Header({ } -export function VisitHistory() { +export function VisitHistory({ + recentVisits, +}: { + recentVisits?: { + id: number; + name: string; + isMinor?: boolean; + }[]; +}) { + recentVisits = recentVisits ?? []; // --- State for Interactivity (Example - not fully implemented in static version) --- const [isDropdownOpen, setIsDropdownOpen] = useState(false); // To control the main dropdown visibility const [activeTab, setActiveTab] = useState('recent'); // 'recent' or 'favorites' for the dropdown tabs - // Dummy data matching the HTML snippet - const recentVisits = [ - { id: 1, name: '장르소설', isMinor: true }, - { id: 2, name: '실시간 베스트', isMinor: false }, - // Add more items as needed - ]; return ( <div className="w-[1160px] mx-auto relative"> {/* Added relative positioning for children */} diff --git a/src/table.tsx b/src/table.tsx index b07ccbf..f95b7ec 100644 --- a/src/table.tsx +++ b/src/table.tsx @@ -1,27 +1,6 @@ +import { AuthorData, NickName } from './AuthorData'; import { cn } from './util/cn'; -export type AuthorData = { - // 운영자 - type: "operator" -} | { - // 고닉 - // 중복 불가능 닉네임 - type: "nickname", - nickname: string, - // 파딱, 주딱 구분을 위한 userType - userType?: "manager" | "submanager" // Optional, if applicable -} | { - // 유동닉 - type: "IP", - // IP 주소 - ip: string, -} | { - // 반유동닉 - // 중복 가능 닉네임 - type: "semi-nickname", - nickname: string, -} - // --- Data Interface (Remains the same) --- export interface TableRowData { id: string | number; @@ -33,19 +12,7 @@ export interface TableRowData { | "icon_ad" | "icon_dctrend"; // e.g., "운영자", "고닉", "반유동", "유동닉" - author?: { - type: "operator" - } | { - type: "nickname", - nickname: string, - userType?: "manager" | "submanager" // Optional, if applicable - } | { - type: "IP", - ip: string, - } | { - type: "semi-nickname", - nickname: string, - } + author?: AuthorData; date: string; @@ -56,7 +23,6 @@ export interface TableRowData { isAdOrSurvey?: boolean; isNews?: boolean; // Handle the last row type specifically if needed titleLinkUrl?: string; // Optional URL for title - authorLinkUrl?: string; // Optional URL for author } // --- Child Component: TableRow --- @@ -64,12 +30,6 @@ interface TableRowProps { rowData: TableRowData; } -const NicknameImagePath = { - "주딱": "/fix_managernik.gif", - "파딱": "/fix_sub_managernik.gif", - "반유동": "/nik.gif", - "default": "/fix_nik.gif" -} export function TableRow({ rowData }: TableRowProps) { const { @@ -85,7 +45,6 @@ export function TableRow({ rowData }: TableRowProps) { isAdOrSurvey, isNews, titleLinkUrl = "#", - authorLinkUrl = "#", author, } = rowData; @@ -139,44 +98,9 @@ export function TableRow({ rowData }: TableRowProps) { <td className={cn( tdCenterClasses, // Base style for author cell 'text-[13px]', // Author cell often uses 13px base font size - authorLinkUrl !== '#' ? 'cursor-pointer' : 'cursor-default', // Conditional cursor )}> - {author?.type === "operator" ? ( - <b className="font-bold">운영자</b> - ) : ( - <> - {/* Author Name Span */} - <span className="inline-block max-w-[81%] align-top text-ellipsis overflow-hidden whitespace-nowrap"> - {/* Inner em/span for potential finer control if needed */} - <em className="not-italic leading-[13px]"> - {author?.type === "nickname" ? author?.nickname : "ㅇㅇ"} - </em> - </span> - {/* Author IP */} - {author?.type === "IP" && ( - <span className="font-tahoma text-[11px] text-custom-gray-medium tracking-[-0.05em] ml-[3px]"> - ({author.ip}) - </span> - )} - {/* Author Icon Placeholder */} - {author?.type !== "IP" && ( - <a href={authorLinkUrl} className="text-custom-gray-dark ml-0.5 inline-block"> - {/* Replace with actual icon component or img tag */} - <img - alt="icon" - src={ - author?.type === "nickname" ? ( - author?.userType === "manager" ? NicknameImagePath["주딱"] : - author?.userType === "submanager" ? NicknameImagePath["파딱"] : - NicknameImagePath["default"] - ) : NicknameImagePath["반유동"] - } - className="align-middle cursor-pointer w-3 h-3" // Example size - /> - </a> - )} - </> - )} + {author && <NickName author={author} />} + {variant === "icon_dctrend" && "디시트렌드"} </td> <td className={tdCenterClasses}>{date}</td> <td className={tdCenterClasses}>{views}</td>