extract props

This commit is contained in:
monoid 2025-04-02 02:10:11 +09:00
parent 7afb21f1f0
commit 4203f06af0
7 changed files with 260 additions and 190 deletions

View file

@ -1,8 +1,9 @@
import './App.css' import './App.css'
import { AnnoymousNickNameProvider } from './AuthorData';
import { CommentInput, CommentItem, CommentListContainer, CommentPagination, PostListControls, SubCommentData } from './Comment'; import { CommentInput, CommentItem, CommentListContainer, CommentPagination, PostListControls, SubCommentData } from './Comment';
import CommentHeader from './CommentHeader'; import CommentHeader from './CommentHeader';
import { Footer } from './Footer'; import { Footer } from './Footer';
import { GalleryContent } from './Gallery'; import { GalleryContent, GalleryContentHeader } from './Gallery';
import { GalleryTitleHeader } from './GalleryTitleHeader'; import { GalleryTitleHeader } from './GalleryTitleHeader';
import { GlobalNavigationBar, Header, VisitHistory } from './Header'; import { GlobalNavigationBar, Header, VisitHistory } from './Header';
import { LoginBox } from './Sidebar'; import { LoginBox } from './Sidebar';
@ -106,7 +107,6 @@ const tableData: TableRowData[] = [
views: "", views: "",
recommendations: "", recommendations: "",
isNews: true, isNews: true,
titleLinkUrl: '#', // Add actual URL
}, },
]; ];
@ -164,66 +164,105 @@ const comments: SubCommentData[] = [
function App() { function App() {
return ( return (
<div> <AnnoymousNickNameProvider value="썬갤러">
<Header /> <div>
<GlobalNavigationBar /> <Header />
<VisitHistory /> <GlobalNavigationBar />
<div className='relative w-[1450px] mx-auto'> <VisitHistory recentVisits={[
<main {
className='w-[1160px] m-[20px_auto_0]' id: 1,
> name: "워썬더",
<section className='' > isMinor: true,
<GalleryTitleHeader /> },
<div {
className='border-custom-blue-dark border w-[1158px]' id: 2,
/> name: "배틀그라운드",
<GalleryContent /> isMinor: false,
<CommentHeader /> },
<CommentListContainer> {
<CommentItem comment={{ id: 3,
id: 1, name: "리그오브레전드",
author: { type: "nickname", nickname: "동아리망했다" }, isMinor: false,
text: '너 지금 상현이햄을 ■■라고', },
timestamp: '03.31 17:44:25', {
showDelete: true, id: 4,
}} /> name: "발로란트",
<CommentItem comment={{ isMinor: false,
id: 2, },
author: { type: "IP", ip: "218.144" }, {
text: '그냥 미국인인데 조센징들은 왜 조선계라고 못 넣어서 안달일까', id: 5,
timestamp: '03.31 18:16:45', name: "오버워치",
showDelete: true, isMinor: true,
subComments: comments, }
}} /> ]} />
<CommentItem comment={{ <div className='relative w-[1450px] mx-auto'>
id: 3, <main
author: { type: "IP", ip: "123.245" }, className='w-[1160px] m-[20px_auto_0]'
text: 'aaa', >
timestamp: '03.31 18:16:45', <section className='' >
showDelete: true, <GalleryTitleHeader title='워썬더 갤러리' />
}} /> <div
</CommentListContainer> className='border-custom-blue-dark border w-[1158px]'
<CommentPagination currentPage={1} maxPage={2} /> />
<CommentInput /> <GalleryContentHeader kind='일반'
<PostListControls owner /> title={'아 e글 유파들이 고도 왜 안올리는지 알았다'}
<div className='flex justify-between'> author={{
<div style={{ type: "IP",
width: "840px", ip: "183.96",
}}> }}
<GalleryTable data={tableData} /> date='2025.02.04 21:32:47'
<PostListControls /> 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>
<div style={{ </section>
width: "300px", </main>
}}> </div>
<LoginBox /> <Footer />
</div> </div >
</div> </AnnoymousNickNameProvider>
</section>
</main>
</div>
<Footer />
</div>
) )
} }

89
src/AuthorData.tsx Normal file
View file

@ -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>
)}
</>
}

View file

@ -1,5 +1,5 @@
import { Separator } from "./Separator"; import { Separator } from "./Separator";
import { AuthorData } from "./table"; import { AuthorData } from './AuthorData';
import { cn } from "./util/cn"; import { cn } from "./util/cn";
import React, { useState } from 'react'; import React, { useState } from 'react';

View file

@ -1,15 +1,37 @@
import { Separator } from "./Separator" 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 ( return (
<header> <header>
{/* Outer container with margin, padding, and bottom border */} {/* Outer container with margin, padding, and bottom border */}
<div className="mt-4 mb-[29px] pb-[11px] border-b border-solid border-gray-200"> <div className="mt-4 mb-[29px] pb-[11px] border-b border-solid border-gray-200">
{/* Post Title */} {/* Post Title */}
<h3 className="px-0.5 mb-[7px] text-sm font-bold"> <h3 className="px-0.5 mb-[7px] text-sm font-bold">
<span> [] </span> <span> [{kind}] </span>
<span> <span>
e글 {title}
</span> </span>
</h3> </h3>
@ -19,16 +41,10 @@ function GalleryContentHeader() {
> >
{/* Left section: Author, IP, Timestamp */} {/* Left section: Author, IP, Timestamp */}
<div> <div>
<span> <NickName author={author} />
{/* 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>
<span className="cursor-default"> <span className="cursor-default">
<Separator /> <Separator />
2025.02.04 21:32:47 {date}
</span> </span>
</div> </div>
@ -37,11 +53,11 @@ function GalleryContentHeader() {
className="pr-[7px]" className="pr-[7px]"
> >
<span className="cursor-default"> <span className="cursor-default">
65 {views}
</span> </span>
<Separator /> <Separator />
<span className="cursor-default"> <span className="cursor-default">
0 {recommendations}
</span> </span>
<Separator /> <Separator />
<span className="cursor-default"> <span className="cursor-default">
@ -50,7 +66,7 @@ function GalleryContentHeader() {
className="inline-block h-5 leading-5 px-2.5 bg-gray-200 className="inline-block h-5 leading-5 px-2.5 bg-gray-200
text-gray-700 border border-gray-300 rounded-full text-gray-700 border border-gray-300 rounded-full
hover:bg-gray-300 hover:border-gray-400 text-xs"> hover:bg-gray-300 hover:border-gray-400 text-xs">
13 {commentCount}
</a> </a>
</span> </span>
</div> </div>
@ -76,16 +92,7 @@ function GalleryRecommendation({
pt-[19px] w-fit box-content"> pt-[19px] w-fit box-content">
<div className="flex items-center justify-center overflow-hidden pb-2"> <div className="flex items-center justify-center overflow-hidden pb-2">
<div className="flex justify-end overflow-hidden w-[139px] mb-0.5"> <div className="flex justify-end overflow-hidden w-[139px] mb-0.5">
<div <div className="text-custom-dropdown-text text-center pt-[10px] pl-[11px] w-[67px] font-bold">
style={{
width: "67px",
paddingTop: "10px",
paddingLeft: "11px",
textAlign: "center",
fontWeight: "bold",
color: "rgb(85, 85, 85)"
}}
>
<p className="text-base leading-[22px] text-[#d31900] font-bold" <p className="text-base leading-[22px] text-[#d31900] font-bold"
>{recommendCount}</p> >{recommendCount}</p>
<p <p
@ -108,16 +115,12 @@ function GalleryRecommendation({
> >
<em <em
style={{ style={{
backgroundColor: "transparent",
backgroundImage:
'url("/sp_image.png")',
backgroundRepeat: "no-repeat",
backgroundPositionX: "0px", backgroundPositionX: "0px",
backgroundPositionY: "-315px", backgroundPositionY: "-315px",
display: "inline-block",
width: "56px", width: "56px",
height: "56px", height: "56px",
}} }}
className="bg-sp-img inline-block"
></em> ></em>
</button> </button>
</div> </div>
@ -276,23 +279,18 @@ export function GalleryContent() {
return <article return <article
className="text-custom-gray-dark text-[13px] font-apple" className="text-custom-gray-dark text-[13px] font-apple"
> >
<GalleryContentHeader />
<div style={{ lineHeight: "22px" }}> <div style={{ lineHeight: "22px" }}>
<div style={{ marginBottom: "50px" }}> <div style={{ marginBottom: "50px" }}>
<div <div className="overflow-hidden relative">
style={{
overflow: "hidden",
position: "relative"
}}
>
<div> <div>
<span style={{ marginLeft: "10px" }}> <span className="ml-[10px]">
<img <img
style={{ style={{
maxWidth: "100%", maxWidth: "100%",
width: "550px", width: "550px",
height: "350px", height: "350px",
}} }}
alt="alt"
/> />
</span> </span>
</div> </div>

View file

@ -1,13 +1,24 @@
import { Separator } from "./Separator" 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 ( return (
<header className="bg-transparent h-[37px] mb-[3px] pt-[4px] text-gray-500"> <header className="bg-transparent h-[37px] mb-[3px] pt-[4px] text-gray-500">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex 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"> <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"> <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 className="bg-sp-img bg-no-repeat inline-block align-top ml-[4px] w-[22px] h-[22px] bg-[-195px_-844px] mt-[3px]">
</div> </div>
</a> </a>
@ -26,14 +37,20 @@ export function GalleryTitleHeader() {
</div> </div>
<Separator /> <Separator />
<button className="cursor-pointer align-top font-sans"> <button className="cursor-pointer align-top font-sans">
(2/8) ({relatedGalleriesCount.current}/{relatedGalleriesCount.total})
<span className="mr-[2px] ml-[2px] hidden"> <span className="mr-[2px] ml-[2px] hidden">
</span> </span>
<em className="bg-sp-img bg-no-repeat inline-block w-[9px] h-[5px] bg-[-115px_-43px] align-[1px] ml-[2px]"> <em className="bg-sp-img bg-no-repeat inline-block w-[9px] h-[5px] bg-[-115px_-43px] align-[1px] ml-[2px]">
</em> </em>
</button> </button>
<Separator /> <Separator />
<button className="cursor-pointer align-top"> <button className="cursor-pointer align-top"
onClick={() => {
prompt("해당 글의 주소입니다.\nCtrl+C를 눌러 클립보드로 복사하세요.",
window.location.href
)
}}
>
</button> </button>
<Separator /> <Separator />

View file

@ -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) --- // --- State for Interactivity (Example - not fully implemented in static version) ---
const [isDropdownOpen, setIsDropdownOpen] = useState(false); // To control the main dropdown visibility const [isDropdownOpen, setIsDropdownOpen] = useState(false); // To control the main dropdown visibility
const [activeTab, setActiveTab] = useState('recent'); // 'recent' or 'favorites' for the dropdown tabs 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 ( return (
<div className="w-[1160px] mx-auto relative"> {/* Added relative positioning for children */} <div className="w-[1160px] mx-auto relative"> {/* Added relative positioning for children */}

View file

@ -1,27 +1,6 @@
import { AuthorData, NickName } from './AuthorData';
import { cn } from './util/cn'; 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) --- // --- Data Interface (Remains the same) ---
export interface TableRowData { export interface TableRowData {
id: string | number; id: string | number;
@ -33,19 +12,7 @@ export interface TableRowData {
| "icon_ad" | "icon_dctrend"; | "icon_ad" | "icon_dctrend";
// e.g., "운영자", "고닉", "반유동", "유동닉" // e.g., "운영자", "고닉", "반유동", "유동닉"
author?: { author?: AuthorData;
type: "operator"
} | {
type: "nickname",
nickname: string,
userType?: "manager" | "submanager" // Optional, if applicable
} | {
type: "IP",
ip: string,
} | {
type: "semi-nickname",
nickname: string,
}
date: string; date: string;
@ -56,7 +23,6 @@ export interface TableRowData {
isAdOrSurvey?: boolean; isAdOrSurvey?: boolean;
isNews?: boolean; // Handle the last row type specifically if needed isNews?: boolean; // Handle the last row type specifically if needed
titleLinkUrl?: string; // Optional URL for title titleLinkUrl?: string; // Optional URL for title
authorLinkUrl?: string; // Optional URL for author
} }
// --- Child Component: TableRow --- // --- Child Component: TableRow ---
@ -64,12 +30,6 @@ interface TableRowProps {
rowData: TableRowData; rowData: TableRowData;
} }
const NicknameImagePath = {
"주딱": "/fix_managernik.gif",
"파딱": "/fix_sub_managernik.gif",
"반유동": "/nik.gif",
"default": "/fix_nik.gif"
}
export function TableRow({ rowData }: TableRowProps) { export function TableRow({ rowData }: TableRowProps) {
const { const {
@ -85,7 +45,6 @@ export function TableRow({ rowData }: TableRowProps) {
isAdOrSurvey, isAdOrSurvey,
isNews, isNews,
titleLinkUrl = "#", titleLinkUrl = "#",
authorLinkUrl = "#",
author, author,
} = rowData; } = rowData;
@ -139,44 +98,9 @@ export function TableRow({ rowData }: TableRowProps) {
<td className={cn( <td className={cn(
tdCenterClasses, // Base style for author cell tdCenterClasses, // Base style for author cell
'text-[13px]', // Author cell often uses 13px base font size 'text-[13px]', // Author cell often uses 13px base font size
authorLinkUrl !== '#' ? 'cursor-pointer' : 'cursor-default', // Conditional cursor
)}> )}>
{author?.type === "operator" ? ( {author && <NickName author={author} />}
<b className="font-bold"></b> {variant === "icon_dctrend" && "디시트렌드"}
) : (
<>
{/* 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>
)}
</>
)}
</td> </td>
<td className={tdCenterClasses}>{date}</td> <td className={tdCenterClasses}>{date}</td>
<td className={tdCenterClasses}>{views}</td> <td className={tdCenterClasses}>{views}</td>