- {/* 검색 및 필터 컨트롤 */}
-
-
-
-
- setSearchQuery(e.target.value)}
- onKeyPress={(e) => e.key === "Enter" && searchPosts()}
- />
-
-
-
-
-
-
-
- {/* 게시물 테이블 */}
- {loading ?
로딩 중...
: renderPostTable()}
+
- {/* 페이지네이션 */}
-
-
- 표시
-
- 항목
-
-
-
-
+ {error && (
+
+ 오류가 발생했습니다: {error.message}
-
+ )}
+
+
{
+ setSelectedPost(post)
+ setShowEditDialog(true)
+ }}
+ onPostDelete={deletePost}
+ onUserClick={openUserModal}
+ />
+
+
- {/* 게시물 추가 대화상자 */}
-
-
- {/* 게시물 수정 대화상자 */}
-
-
- {/* 댓글 추가 대화상자 */}
-
-
- {/* 댓글 수정 대화상자 */}
-
-
- {/* 게시물 상세 보기 대화상자 */}
-
-
- {/* 사용자 모달 */}
-
+
+
+
+
+
+
+
+
+
+ {selectedPost && commentsData && (
+ {
+ const comment = commentsData.comments.find((c) => c.id === id)
+ if (comment) {
+ // likes가 null이거나 undefined인 경우 0으로 처리
+ const currentLikes = comment.likes ?? 0
+ likeComment(id, postId, currentLikes)
+ }
+ }}
+ />
+ )}
+
+
+
)
}
-export default PostsManager
+export default PostsManagerPage
diff --git a/src/pages/model/useUrlSync.ts b/src/pages/model/useUrlSync.ts
new file mode 100644
index 000000000..00828f714
--- /dev/null
+++ b/src/pages/model/useUrlSync.ts
@@ -0,0 +1,81 @@
+import { useEffect, useRef } from "react"
+import { useLocation, useNavigate } from "react-router-dom"
+import { useAtom } from "jotai"
+import { skipAtom, limitAtom } from "../../features/post-list"
+import { searchQueryAtom, selectedTagAtom, sortByAtom, orderAtom } from "../../features/post-search"
+import { parsePostListParams, buildPostListUrl } from "../../shared/lib"
+
+/**
+ * URL 파라미터와 atoms를 동기화하는 hook
+ * - 초기 마운트 시 URL → atoms
+ * - atoms 변경 시 → URL 업데이트
+ * - 브라우저 뒤로가기/앞으로가기 시 URL → atoms
+ */
+export const useUrlSync = () => {
+ const navigate = useNavigate()
+ const location = useLocation()
+ const isInitialMount = useRef(true)
+ const isInitialized = useRef(false)
+
+ // Atoms
+ const [skip, setSkip] = useAtom(skipAtom)
+ const [limit, setLimit] = useAtom(limitAtom)
+ const [searchQuery, setSearchQuery] = useAtom(searchQueryAtom)
+ const [sortBy, setSortBy] = useAtom(sortByAtom)
+ const [order, setOrder] = useAtom(orderAtom)
+ const [selectedTag, setSelectedTag] = useAtom(selectedTagAtom)
+
+ // 초기 마운트 시 URL 파라미터를 먼저 읽어서 atom 설정
+ useEffect(() => {
+ if (isInitialized.current) return
+
+ // 순수함수를 사용하여 URL 파라미터 파싱
+ const urlParams = parsePostListParams(location.search)
+
+ // atom 설정 (동기적으로 실행되지만 다음 렌더에서 반영됨)
+ setSkip(urlParams.skip)
+ setLimit(urlParams.limit)
+ setSearchQuery(urlParams.search)
+ setSortBy(urlParams.sortBy)
+ setOrder(urlParams.order)
+ setSelectedTag(urlParams.tag)
+
+ isInitialized.current = true
+
+ // 다음 렌더 사이클에서 초기 마운트 완료로 표시
+ setTimeout(() => {
+ isInitialMount.current = false
+ }, 0)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ // 상태 변경 시 URL 업데이트
+ useEffect(() => {
+ if (isInitialMount.current) return
+
+ // 순수함수를 사용하여 URL 생성
+ const queryString = buildPostListUrl({
+ skip,
+ limit,
+ search: searchQuery,
+ sortBy,
+ order,
+ tag: selectedTag,
+ })
+ navigate(`?${queryString}`, { replace: true })
+ }, [skip, limit, sortBy, order, selectedTag, navigate, searchQuery])
+
+ // URL 변경 감지 (뒤로가기/앞으로가기)
+ useEffect(() => {
+ if (isInitialMount.current) return
+
+ // 순수함수를 사용하여 URL 파라미터 파싱
+ const urlParams = parsePostListParams(location.search)
+ setSkip(urlParams.skip)
+ setLimit(urlParams.limit)
+ setSearchQuery(urlParams.search)
+ setSortBy(urlParams.sortBy)
+ setOrder(urlParams.order)
+ setSelectedTag(urlParams.tag)
+ }, [location.search, setSkip, setLimit, setSearchQuery, setSortBy, setOrder, setSelectedTag])
+}
diff --git a/src/shared/lib/apiConfig.ts b/src/shared/lib/apiConfig.ts
new file mode 100644
index 000000000..f068853de
--- /dev/null
+++ b/src/shared/lib/apiConfig.ts
@@ -0,0 +1,11 @@
+// API Base URL 설정
+// 개발 환경: Vite proxy 사용 (/api)
+// 프로덕션: DummyJSON API 직접 호출
+
+// @ts-ignore - Vite 환경 변수
+const isProduction = import.meta.env?.PROD === true
+
+export const API_BASE_URL = isProduction
+ ? "https://dummyjson.com" // 프로덕션: DummyJSON 직접 호출
+ : "/api" // 개발 환경: Vite proxy 사용
+
diff --git a/src/shared/lib/dataTransform.ts b/src/shared/lib/dataTransform.ts
new file mode 100644
index 000000000..4e27b9a46
--- /dev/null
+++ b/src/shared/lib/dataTransform.ts
@@ -0,0 +1,15 @@
+import { Post } from "../../entities/post/model/types"
+import { User } from "../../entities/user/model/types"
+
+/**
+ * 게시물 배열에 작성자 정보를 추가하는 순수함수
+ * @param posts 게시물 배열
+ * @param users 사용자 배열
+ * @returns 작성자 정보가 추가된 새로운 게시물 배열 (원본 배열은 변경하지 않음)
+ */
+export const enrichPostsWithAuthors = (posts: Post[], users: User[]): Post[] => {
+ return posts.map((post) => ({
+ ...post,
+ author: users.find((user) => user.id === post.userId),
+ }))
+}
diff --git a/src/shared/lib/dateUtils.ts b/src/shared/lib/dateUtils.ts
new file mode 100644
index 000000000..e1ade7d07
--- /dev/null
+++ b/src/shared/lib/dateUtils.ts
@@ -0,0 +1,63 @@
+/**
+ * 날짜를 YYYY.MM.DD 형식으로 포맷팅하는 순수함수
+ * @param date Date 객체, ISO 문자열, 또는 타임스탬프
+ * @returns 포맷팅된 날짜 문자열 (예: "2024.01.15")
+ */
+export const formatDate = (date: Date | string | number): string => {
+ const d = new Date(date)
+ if (isNaN(d.getTime())) {
+ return ""
+ }
+ const year = d.getFullYear()
+ const month = String(d.getMonth() + 1).padStart(2, "0")
+ const day = String(d.getDate()).padStart(2, "0")
+ return `${year}.${month}.${day}`
+}
+
+/**
+ * 날짜를 YYYY-MM-DD HH:mm:ss 형식으로 포맷팅하는 순수함수
+ * @param date Date 객체, ISO 문자열, 또는 타임스탬프
+ * @returns 포맷팅된 날짜시간 문자열 (예: "2024-01-15 14:30:00")
+ */
+export const formatDateTime = (date: Date | string | number): string => {
+ const d = new Date(date)
+ if (isNaN(d.getTime())) {
+ return ""
+ }
+ const year = d.getFullYear()
+ const month = String(d.getMonth() + 1).padStart(2, "0")
+ const day = String(d.getDate()).padStart(2, "0")
+ const hours = String(d.getHours()).padStart(2, "0")
+ const minutes = String(d.getMinutes()).padStart(2, "0")
+ const seconds = String(d.getSeconds()).padStart(2, "0")
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+}
+
+/**
+ * 날짜를 상대적 시간으로 포맷팅하는 순수함수 (예: "3일 전", "2시간 전")
+ * @param date Date 객체, ISO 문자열, 또는 타임스탬프
+ * @returns 상대적 시간 문자열
+ */
+export const formatRelativeTime = (date: Date | string | number): string => {
+ const d = new Date(date)
+ if (isNaN(d.getTime())) {
+ return ""
+ }
+ const now = new Date()
+ const diff = now.getTime() - d.getTime()
+ const seconds = Math.floor(diff / 1000)
+ const minutes = Math.floor(seconds / 60)
+ const hours = Math.floor(minutes / 60)
+ const days = Math.floor(hours / 24)
+ const weeks = Math.floor(days / 7)
+ const months = Math.floor(days / 30)
+ const years = Math.floor(days / 365)
+
+ if (years > 0) return `${years}년 전`
+ if (months > 0) return `${months}개월 전`
+ if (weeks > 0) return `${weeks}주 전`
+ if (days > 0) return `${days}일 전`
+ if (hours > 0) return `${hours}시간 전`
+ if (minutes > 0) return `${minutes}분 전`
+ return "방금 전"
+}
diff --git a/src/shared/lib/highlightText.tsx b/src/shared/lib/highlightText.tsx
new file mode 100644
index 000000000..24cfca495
--- /dev/null
+++ b/src/shared/lib/highlightText.tsx
@@ -0,0 +1,14 @@
+export const highlightText = (text: string, highlight: string) => {
+ if (!text) return null
+ if (!highlight.trim()) {
+ return
{text}
+ }
+ const regex = new RegExp(`(${highlight})`, "gi")
+ const parts = text.split(regex)
+ return (
+
+ {parts.map((part, i) => (regex.test(part) ? {part} : {part}))}
+
+ )
+}
+
diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts
new file mode 100644
index 000000000..0db4c3f24
--- /dev/null
+++ b/src/shared/lib/index.ts
@@ -0,0 +1,8 @@
+export * from "./highlightText"
+export * from "./queryClient"
+export * from "./apiConfig"
+export * from "./sortUtils"
+export * from "./urlUtils"
+export * from "./dataTransform"
+export * from "./dateUtils"
+
diff --git a/src/shared/lib/queryClient.ts b/src/shared/lib/queryClient.ts
new file mode 100644
index 000000000..04ff7bb87
--- /dev/null
+++ b/src/shared/lib/queryClient.ts
@@ -0,0 +1,14 @@
+import { QueryClient } from "@tanstack/react-query"
+
+export const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 1000 * 60 * 5, // 5분
+ gcTime: 1000 * 60 * 10, // 10분 (구 cacheTime)
+ retry: 1,
+ refetchOnWindowFocus: false,
+ refetchOnMount: true, // 마운트 시 항상 최신 데이터 확인
+ },
+ },
+})
+
diff --git a/src/shared/lib/sortUtils.ts b/src/shared/lib/sortUtils.ts
new file mode 100644
index 000000000..dd4a7215c
--- /dev/null
+++ b/src/shared/lib/sortUtils.ts
@@ -0,0 +1,16 @@
+import { Post } from "../../entities/post/model/types"
+
+/**
+ * 게시물을 reactions(좋아요) 기준으로 정렬하는 순수함수
+ * @param posts 정렬할 게시물 배열
+ * @param order 정렬 순서 ('asc' | 'desc')
+ * @returns 정렬된 새로운 게시물 배열 (원본 배열은 변경하지 않음)
+ */
+export const sortPostsByReactions = (posts: Post[], order: "asc" | "desc"): Post[] => {
+ return [...posts].sort((a, b) => {
+ const aLikes = a.reactions?.likes || 0
+ const bLikes = b.reactions?.likes || 0
+
+ return order === "asc" ? aLikes - bLikes : bLikes - aLikes
+ })
+}
diff --git a/src/shared/lib/urlUtils.ts b/src/shared/lib/urlUtils.ts
new file mode 100644
index 000000000..f3c35f3c1
--- /dev/null
+++ b/src/shared/lib/urlUtils.ts
@@ -0,0 +1,58 @@
+/**
+ * 게시물 목록 URL 파라미터 타입
+ */
+export interface PostListParams {
+ skip: number
+ limit: number
+ search: string
+ sortBy: string
+ order: string
+ tag: string
+}
+
+/**
+ * URL search string을 파싱하여 PostListParams 객체로 변환하는 순수함수
+ * @param search URL search string (예: "?skip=0&limit=10&search=test")
+ * @returns 파싱된 파라미터 객체
+ */
+export const parsePostListParams = (search: string): PostListParams => {
+ const params = new URLSearchParams(search)
+ return {
+ skip: parseInt(params.get("skip") || "0", 10),
+ limit: parseInt(params.get("limit") || "10", 10),
+ search: params.get("search") || "",
+ sortBy: params.get("sortBy") || "none",
+ order: params.get("order") || "asc",
+ tag: params.get("tag") || "",
+ }
+}
+
+/**
+ * PostListParams 객체를 URL query string으로 변환하는 순수함수
+ * @param params 파라미터 객체 (부분 객체도 가능)
+ * @returns URL query string (예: "skip=0&limit=10&search=test")
+ */
+export const buildPostListUrl = (params: Partial
): string => {
+ const urlParams = new URLSearchParams()
+
+ if (params.skip !== undefined && params.skip > 0) {
+ urlParams.set("skip", params.skip.toString())
+ }
+ if (params.limit !== undefined && params.limit !== 10) {
+ urlParams.set("limit", params.limit.toString())
+ }
+ if (params.search) {
+ urlParams.set("search", params.search)
+ }
+ if (params.sortBy && params.sortBy !== "none") {
+ urlParams.set("sortBy", params.sortBy)
+ }
+ if (params.order && params.order !== "asc") {
+ urlParams.set("order", params.order)
+ }
+ if (params.tag) {
+ urlParams.set("tag", params.tag)
+ }
+
+ return urlParams.toString()
+}
diff --git a/src/shared/ui/Button.tsx b/src/shared/ui/Button.tsx
new file mode 100644
index 000000000..837821d3e
--- /dev/null
+++ b/src/shared/ui/Button.tsx
@@ -0,0 +1,40 @@
+import * as React from "react"
+import { forwardRef } from "react"
+import { cva, VariantProps } from "class-variance-authority"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
+ {
+ variants: {
+ variant: {
+ default: "bg-blue-500 text-white hover:bg-blue-600",
+ destructive: "bg-red-500 text-white hover:bg-red-600",
+ outline: "border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-100",
+ secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
+ ghost: "bg-transparent text-gray-700 hover:bg-gray-100",
+ link: "underline-offset-4 hover:underline text-blue-500",
+ },
+ size: {
+ default: "h-10 py-2 px-4",
+ sm: "h-8 px-3 rounded-md text-xs",
+ lg: "h-11 px-8 rounded-md",
+ icon: "h-9 w-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+)
+
+interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps {
+ className?: string
+}
+
+export const Button = forwardRef(({ className, variant, size, ...props }, ref) => {
+ return
+})
+
+Button.displayName = "Button"
+
diff --git a/src/shared/ui/Card.tsx b/src/shared/ui/Card.tsx
new file mode 100644
index 000000000..3126de396
--- /dev/null
+++ b/src/shared/ui/Card.tsx
@@ -0,0 +1,25 @@
+import * as React from "react"
+import { forwardRef } from "react"
+
+export const Card = forwardRef>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+export const CardHeader = forwardRef>(
+ ({ className, ...props }, ref) => ,
+)
+CardHeader.displayName = "CardHeader"
+
+export const CardTitle = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+)
+CardTitle.displayName = "CardTitle"
+
+export const CardContent = forwardRef>(
+ ({ className, ...props }, ref) => ,
+)
+CardContent.displayName = "CardContent"
+
diff --git a/src/shared/ui/Dialog.tsx b/src/shared/ui/Dialog.tsx
new file mode 100644
index 000000000..d024e2f4f
--- /dev/null
+++ b/src/shared/ui/Dialog.tsx
@@ -0,0 +1,48 @@
+import * as React from "react"
+import { forwardRef } from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
+
+export const Dialog = DialogPrimitive.Root
+export const DialogTrigger = DialogPrimitive.Trigger
+export const DialogPortal = DialogPrimitive.Portal
+export const DialogOverlay = DialogPrimitive.Overlay
+
+export const DialogContent = forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ 닫기
+
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+export const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+export const DialogTitle = forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
diff --git a/src/shared/ui/ErrorBoundary.tsx b/src/shared/ui/ErrorBoundary.tsx
new file mode 100644
index 000000000..a5926b9db
--- /dev/null
+++ b/src/shared/ui/ErrorBoundary.tsx
@@ -0,0 +1,47 @@
+import { Component, ErrorInfo, ReactNode } from "react"
+import { Card, CardContent, CardHeader, CardTitle } from "./Card"
+import { Button } from "./Button"
+
+interface Props {
+ children: ReactNode
+}
+
+interface State {
+ hasError: boolean
+ error?: Error
+}
+
+export class ErrorBoundary extends Component {
+ public state: State = {
+ hasError: false,
+ }
+
+ public static getDerivedStateFromError(error: Error): State {
+ return { hasError: true, error }
+ }
+
+ public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+ console.error("Uncaught error:", error, errorInfo)
+ }
+
+ public render() {
+ if (this.state.hasError) {
+ return (
+
+
+ 오류가 발생했습니다
+
+
+
+
{this.state.error?.message}
+
+
+
+
+ )
+ }
+
+ return this.props.children
+ }
+}
+
diff --git a/src/shared/ui/Input.tsx b/src/shared/ui/Input.tsx
new file mode 100644
index 000000000..9907c9967
--- /dev/null
+++ b/src/shared/ui/Input.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+import { forwardRef } from "react"
+
+export const Input = forwardRef>(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ },
+)
+
+Input.displayName = "Input"
+
diff --git a/src/shared/ui/Select.tsx b/src/shared/ui/Select.tsx
new file mode 100644
index 000000000..d245846cf
--- /dev/null
+++ b/src/shared/ui/Select.tsx
@@ -0,0 +1,60 @@
+import * as React from "react"
+import { forwardRef } from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { Check, ChevronDown } from "lucide-react"
+
+export const Select = SelectPrimitive.Root
+export const SelectGroup = SelectPrimitive.Group
+export const SelectValue = SelectPrimitive.Value
+
+export const SelectTrigger = forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+export const SelectContent = forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+ {children}
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+export const SelectItem = forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
diff --git a/src/shared/ui/Table.tsx b/src/shared/ui/Table.tsx
new file mode 100644
index 000000000..092c73bd4
--- /dev/null
+++ b/src/shared/ui/Table.tsx
@@ -0,0 +1,51 @@
+import * as React from "react"
+import { forwardRef } from "react"
+
+export const Table = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+)
+Table.displayName = "Table"
+
+export const TableHeader = forwardRef>(
+ ({ className, ...props }, ref) => ,
+)
+TableHeader.displayName = "TableHeader"
+
+export const TableBody = forwardRef>(
+ ({ className, ...props }, ref) => ,
+)
+TableBody.displayName = "TableBody"
+
+export const TableRow = forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+)
+TableRow.displayName = "TableRow"
+
+export const TableHead = forwardRef>(
+ ({ className, ...props }, ref) => (
+ |
+ ),
+)
+TableHead.displayName = "TableHead"
+
+export const TableCell = forwardRef>(
+ ({ className, ...props }, ref) => (
+ |
+ ),
+)
+TableCell.displayName = "TableCell"
+
diff --git a/src/shared/ui/Textarea.tsx b/src/shared/ui/Textarea.tsx
new file mode 100644
index 000000000..f656d36ff
--- /dev/null
+++ b/src/shared/ui/Textarea.tsx
@@ -0,0 +1,17 @@
+import * as React from "react"
+import { forwardRef } from "react"
+
+export const Textarea = forwardRef>(
+ ({ className, ...props }, ref) => {
+ return (
+
+ )
+ },
+)
+
+Textarea.displayName = "Textarea"
+
diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts
new file mode 100644
index 000000000..fefb43c5e
--- /dev/null
+++ b/src/shared/ui/index.ts
@@ -0,0 +1,9 @@
+export * from "./Button"
+export * from "./Input"
+export * from "./Textarea"
+export * from "./Card"
+export * from "./Select"
+export * from "./Dialog"
+export * from "./Table"
+export * from "./ErrorBoundary"
+
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 000000000..172d029eb
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1,7 @@
+///
+
+declare module "*.css" {
+ const content: string
+ export default content
+}
+
diff --git a/src/widgets/footer/index.ts b/src/widgets/footer/index.ts
new file mode 100644
index 000000000..fcaecd91b
--- /dev/null
+++ b/src/widgets/footer/index.ts
@@ -0,0 +1,2 @@
+export { Footer } from "./ui/Footer"
+
diff --git a/src/widgets/footer/ui/Footer.tsx b/src/widgets/footer/ui/Footer.tsx
new file mode 100644
index 000000000..0d0a2601d
--- /dev/null
+++ b/src/widgets/footer/ui/Footer.tsx
@@ -0,0 +1,12 @@
+import React from "react"
+
+export const Footer: React.FC = () => {
+ return (
+
+ )
+}
+
diff --git a/src/widgets/header/index.ts b/src/widgets/header/index.ts
new file mode 100644
index 000000000..bba297265
--- /dev/null
+++ b/src/widgets/header/index.ts
@@ -0,0 +1,2 @@
+export { Header } from "./ui/Header"
+
diff --git a/src/widgets/header/ui/Header.tsx b/src/widgets/header/ui/Header.tsx
new file mode 100644
index 000000000..8a518628c
--- /dev/null
+++ b/src/widgets/header/ui/Header.tsx
@@ -0,0 +1,35 @@
+import React from "react"
+import { MessageSquare } from "lucide-react"
+
+export const Header: React.FC = () => {
+ return (
+
+
+
+
+
게시물 관리 시스템
+
+
+
+
+ )
+}
+
diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo
new file mode 100644
index 000000000..71ffffa63
--- /dev/null
+++ b/tsconfig.app.tsbuildinfo
@@ -0,0 +1 @@
+{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/entities/comment/index.ts","./src/entities/comment/api/api.ts","./src/entities/comment/model/queries.ts","./src/entities/comment/model/store.ts","./src/entities/comment/model/types.ts","./src/entities/comment/ui/commentitem.tsx","./src/entities/comment/ui/index.ts","./src/entities/post/index.ts","./src/entities/post/api/api.ts","./src/entities/post/model/queries.ts","./src/entities/post/model/store.ts","./src/entities/post/model/types.ts","./src/entities/post/ui/postcard.tsx","./src/entities/post/ui/index.ts","./src/entities/tag/index.ts","./src/entities/tag/api/api.ts","./src/entities/tag/model/queries.ts","./src/entities/tag/model/store.ts","./src/entities/tag/model/types.ts","./src/entities/user/index.ts","./src/entities/user/api/api.ts","./src/entities/user/model/queries.ts","./src/entities/user/model/store.ts","./src/entities/user/model/types.ts","./src/entities/user/ui/useravatar.tsx","./src/entities/user/ui/index.ts","./src/features/comment-management/index.ts","./src/features/comment-management/model/index.ts","./src/features/comment-management/model/usecomments.ts","./src/features/comment-management/ui/addcommentdialog.tsx","./src/features/comment-management/ui/commentlist.tsx","./src/features/comment-management/ui/editcommentdialog.tsx","./src/features/pagination/index.ts","./src/features/pagination/ui/pagination.tsx","./src/features/post-add/index.ts","./src/features/post-add/model/index.ts","./src/features/post-add/model/useaddpost.ts","./src/features/post-add/ui/addpostdialog.tsx","./src/features/post-detail/index.ts","./src/features/post-detail/ui/postdetaildialog.tsx","./src/features/post-edit/index.ts","./src/features/post-edit/model/index.ts","./src/features/post-edit/model/useeditpost.ts","./src/features/post-edit/ui/editpostdialog.tsx","./src/features/post-list/index.ts","./src/features/post-list/model/index.ts","./src/features/post-list/model/store.ts","./src/features/post-list/model/usepostlist.ts","./src/features/post-list/ui/posttable.tsx","./src/features/post-search/index.ts","./src/features/post-search/model/index.ts","./src/features/post-search/model/store.ts","./src/features/post-search/model/usetags.ts","./src/features/post-search/ui/postsearchcontrols.tsx","./src/features/user-profile/index.ts","./src/features/user-profile/model/index.ts","./src/features/user-profile/model/useuserprofile.ts","./src/features/user-profile/ui/userprofiledialog.tsx","./src/pages/postsmanagerpage.tsx","./src/shared/lib/apiconfig.ts","./src/shared/lib/highlighttext.tsx","./src/shared/lib/index.ts","./src/shared/lib/queryclient.ts","./src/shared/ui/button.tsx","./src/shared/ui/card.tsx","./src/shared/ui/dialog.tsx","./src/shared/ui/errorboundary.tsx","./src/shared/ui/input.tsx","./src/shared/ui/select.tsx","./src/shared/ui/table.tsx","./src/shared/ui/textarea.tsx","./src/shared/ui/index.ts","./src/widgets/footer/index.ts","./src/widgets/footer/ui/footer.tsx","./src/widgets/header/index.ts","./src/widgets/header/ui/header.tsx","./src/widgets/posts-manager/index.ts","./src/widgets/posts-manager/model/store.ts","./src/widgets/posts-manager/ui/postsmanagerwidget.tsx"],"version":"5.9.3"}
\ No newline at end of file
diff --git a/tsconfig.node.tsbuildinfo b/tsconfig.node.tsbuildinfo
new file mode 100644
index 000000000..62c7bf924
--- /dev/null
+++ b/tsconfig.node.tsbuildinfo
@@ -0,0 +1 @@
+{"root":["./vite.config.ts"],"version":"5.9.3"}
\ No newline at end of file
diff --git a/vite-env.d.ts b/vite-env.d.ts
index 11f02fe2a..2e206a756 100644
--- a/vite-env.d.ts
+++ b/vite-env.d.ts
@@ -1 +1,6 @@
///
+
+declare module "*.css" {
+ const content: string
+ export default content
+}
diff --git a/vite.config.ts b/vite.config.ts
index be7b7a3d4..5c8ab21cd 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -2,8 +2,9 @@ import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
// https://vite.dev/config/
-export default defineConfig({
+export default defineConfig(({ mode }) => ({
plugins: [react()],
+ base: mode === "production" ? "/front_7th_chapter3-3/" : "/",
server: {
proxy: {
"/api": {
@@ -14,4 +15,4 @@ export default defineConfig({
},
},
},
-})
+}))