Merge commit 'a5c3405049094e015c7e159e4b1c65a4b851396b' into iipbs/sn/oi

This commit is contained in:
JIeunhuh
2025-04-16 09:48:28 +09:00
11 changed files with 183 additions and 2047 deletions
@@ -63,7 +63,7 @@
"eslint-plugin-react-refresh": "^0.4.16",
"eslint-plugin-storybook": "^0.11.2",
"globals": "^15.14.0",
"postcss": "^8.5.1",
"postcss": "^8.5.3",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"storybook": "^8.5.3",
@@ -1,69 +1,10 @@
@import "@common/assets/styles/base.css";
@import "@common/assets/styles/diveicon.css";
@import "@common/assets/styles/components.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: #ffffff;
--foreground: #0a0a0a;
--card: #ffffff;
--card-foreground: #0a0a0a;
--popover: #ffffff;
--popover-foreground: #0a0a0a;
--primary: #2094fa;
--primary-foreground: #fafafa;
--secondary: #f5f5f5;
--secondary-foreground: #0f1e33;
--muted: #f5f5f5;
--muted-foreground: #737373;
--accent: #f5f5f5;
--accent-foreground: #0f1e33;
--destructive: #ff4141;
--destructive-foreground: #fafafa;
--border: #e5e5e5;
--input: #e5e5e5;
--label: #666666;
--ring: #0a0a0a;
--chart-1: #d95b43;
--chart-2: #299392;
--chart-3: #263e4d;
--chart-4: #dfb938;
--chart-5: #f08634;
--radius: 1rem;
}
[data-mode="dark"] {
--background: #0a0a0a;
--foreground: #fafafa;
--card: #0a0a0a;
--card-foreground: #fafafa;
--popover: #0a0a0a;
--popover-foreground: #fafafa;
--primary: #2094fa;
--primary-foreground: #171717;
--secondary: #262626;
--secondary-foreground: #fafafa;
--muted: #262626;
--muted-foreground: #a2a2a2;
--accent: #262626;
--accent-foreground: #fafafa;
--destructive: #b22222;
--destructive-foreground: #fafafa;
--border: #262626;
--input: #262626;
--label: #f1f1f1;
--ring: #d4d4d4;
--chart-1: #3399ff;
--chart-2: #339966;
--chart-3: #ff9933;
--chart-4: #cc66ff;
--chart-5: #ff3399;
}
}
@layer base {
* {
@apply border-border;
@@ -74,4 +15,4 @@
body {
@apply bg-background text-foreground;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,11 +0,0 @@
@layer base {}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
@@ -9,12 +9,23 @@ import type { DayPickerSingleProps } from "react-day-picker";
import { Button, Calendar, Popover, PopoverContent, PopoverTrigger } from "@common/components/ui";
import { cn } from "../../lib/utils";
import { FormControl } from "../form";
import { ko } from "date-fns/locale";
import { useState } from "react";
type DatePickerProps = DayPickerSingleProps & { field: { value: Date; onChange: (date: Date | undefined) => void } ,selectClass:string|undefined };
export function DatePicker({ field, selectClass }: DatePickerProps) {
const [open, setOpen] = useState(false);
const handleSelect = (date: Date | undefined) => {
field.onChange(date); // React Hook Form 업데이트
setOpen(false);
};
return (
<Popover>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<FormControl>
<Button
@@ -27,7 +38,12 @@ export function DatePicker({ field, selectClass }: DatePickerProps) {
):selectClass}
>
{field?.value ? (
format(field?.value, "PPP")
format(
field.value instanceof Date && !isNaN(field.value.getTime())
? field.value
: new Date()
, "PPP", { locale: ko })
) : (
<span>Pick a date</span>
)}
@@ -39,8 +55,9 @@ export function DatePicker({ field, selectClass }: DatePickerProps) {
<Calendar
mode="single"
selected={field?.value}
onSelect={field?.onChange}
onSelect={handleSelect}
initialFocus
locale={ko}
/>
</PopoverContent>
</Popover>
@@ -1,52 +1,47 @@
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@common/components";
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTrigger } from "@common/components";
import { useState } from "react";
type ModalType = {
bodyClass: string;
butNm: string;
title: string;
body: React.ReactNode;
open?: boolean;
onOpenChange?: (open: boolean) => void;
noHeaderYn?: 'N' | 'Y';
noFooterYn?: 'N' | 'Y';
headerTitle?: string;
footerContent?: React.ReactNode;
}
export function UiModal({ bodyClass, butNm, title, body, open, onOpenChange }: ModalType) {
const dialogProps = open !== undefined && onOpenChange !== undefined
? { open, onOpenChange }
: {};
export function UiModal({
bodyClass,
body,
open,
onOpenChange,
noHeaderYn = 'N',
noFooterYn = 'N',
headerTitle,
footerContent
}: ModalType) {
return (
<>
{!open && (
<Dialog>
<DialogTrigger asChild>
<Button className="mr-4 rounded-lg">{butNm}</Button>
</DialogTrigger>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={bodyClass}>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
{body}
<DialogFooter className="sm:justify-start">
</DialogFooter>
{noHeaderYn === 'N' && (
<DialogHeader>
<DialogDescription>
{headerTitle}
</DialogDescription>
</DialogHeader>
)}
{(open && body)}
{noFooterYn === 'N' && (
<DialogFooter className="sm:justify-start">
{footerContent}
</DialogFooter>
)}
</DialogContent>
</Dialog>
)}
{open && (
<Dialog {...dialogProps}>
<DialogContent className={bodyClass}>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
{body}
<DialogFooter className="sm:justify-start">
</DialogFooter>
</DialogContent>
</Dialog>
)}
</>
);
}
@@ -1,8 +1,8 @@
import React from 'react'
function ListDiv(props: {children: React.ReactNode}) {
function ListDiv(props: {children: React.ReactNode, className?: string|undefined}) {
return (
<div className="card card-border">
<div className={`card card-border ${props.className}`}>
{props.children}
</div>
)
@@ -3,13 +3,26 @@ import React from "react";
const SearchInput = React.forwardRef<HTMLInputElement, {className:string, label:string, textValue:string, setText:(val:string)=>void}>(({className, label, textValue, setText}, ref) => {
const changeText=(e:React.ChangeEvent<HTMLInputElement>)=>{
if(e?.target?.value) setText(e.target.value)
}
if(e?.target?.value){
setText(e.target.value)
}
}
const keyUp=(e:any)=>{
if(e.keyCode===8 && e.target.value.length==1){
setText('')
}
}
return (
<div className={className+" flex items-center"} >
{label && <Label className="w-20 text-base text-gray-600">{label}</Label>}
<Input className="flex-1 ml-8 w-3/4 h-10 border rounded" value={textValue} onChange={changeText}></Input>
<Input className="flex-1 ml-8 w-3/4 h-10 border rounded" value={textValue} onChange={changeText} onKeyUp={keyUp}></Input>
</div>
)
});
@@ -1,30 +1,55 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
import { useEffect, useState } from "react";
import UiPageNations from "./uiPageNations";
type UiTableProps = {
headName: React.ReactNode | undefined;
tableData: { [key: string]: string | number | boolean }[];
tableData: { [key: string]: string | number | boolean | object}[];
headlist: UiHeadProps[];
pageSize: number;
total: number;
children: React.ReactNode | undefined;
cellClick : ((index:number|undefined)=>void) | undefined;
cellClick : ((index:number|undefined, headId:string)=>void )| undefined;
onPageChange: (page: number) => void;
className?: string;
resetPageNo?: boolean;
}
type UiHeadProps = {
id: string;
title: string;
align?: string;
}
export function UiTable({ tableData, headlist, pageSize, total, children, headName, cellClick }: UiTableProps) {
const cur = 1;
export function UiTable({ tableData, headlist, pageSize, total, children, headName, cellClick, onPageChange, className = '', resetPageNo = false }: UiTableProps) {
const [cur, setCur] = useState<number>(1);
const [idx, setIdx] = useState<number>(-1);
useEffect(() => {
if (resetPageNo) {
setCur(1);
onPageChange(1);
}
}, [onPageChange, resetPageNo]);
const handlePageChange = (page: number) => {
setCur(page);
onPageChange(page);
}
const handleCellClick = (index: number,headId:string )=> {
if (cellClick) {
cellClick(index,headId);
setIdx(index);
}
};
return (
<>
{headName ? <div className="card-header">
<h3 className="text-2xl font-medium text-label">{headName}</h3>
</div> : <></>}
<div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0r">
<div className={`card !m-[1.2rem] !p-0r ${className}`}>
<Table>
<TableHeader>
{children ? <>{children}</> : <TableRow>
@@ -34,17 +59,26 @@ export function UiTable({ tableData, headlist, pageSize, total, children, headNa
</TableRow>}
</TableHeader>
<TableBody className='text-center'>
{tableData?.map((el: { [key: string]: string | number | boolean }, index) => (
<TableRow key={index + "row"} >
{headlist.map((head) => (
<TableCell className="cursor-pointer" onClick={()=>cellClick && cellClick(index)} key={head.id + index}>{el[head.id]}</TableCell>
{tableData?.map((el: { [key: string]: string | number | boolean | object}, index) => (
<TableRow key={index + "row"}>
{headlist?.map((head) => (
<TableCell
className={`${head.align === 'right' ? 'text-right' : 'text-center'} ${
cellClick && idx === index ? 'opacity-40 cursor-pointer' :
cellClick ? 'cursor-pointer' : ''
}`}
onClick={() => handleCellClick(index, head.id)}
key={head.id + index}
>
{String(el[head.id] ?? '')}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
{total <= 0 ? <></> : <UiPageNations total={total} cur={cur} pageSize={pageSize} />}
{total <= 0 ? <></> : <UiPageNations total={total} cur={cur} pageSize={pageSize} onPageChange={handlePageChange} />}
</div>
</>
);
}
}
@@ -1,46 +1,77 @@
'use client'
'use client';
import { useEffect, useState } from "react";
import { Pagination, PaginationContent, PaginationItem, PaginationPrevious, PaginationLink, PaginationNext } from "./lib/pagination";
import { PaginationNext } from "./lib/pagination";
import { PaginationLink } from "./lib/pagination";
import { PaginationPrevious } from "./lib/pagination";
import { PaginationItem } from "./lib/pagination";
import { PaginationContent } from "./lib/pagination";
import { Pagination } from "./lib/pagination";
interface CommonPageNationsProps {
total: number;
cur: number;
pageSize: number;
total: number;
cur: number;
pageSize: number;
onPageChange: (page: number) => void;
}
export default function UiPageNations(commonPageNationsProps: CommonPageNationsProps) {
const [pageProps, setPageProps] = useState(commonPageNationsProps);
const [pageProps, setPageProps] = useState(commonPageNationsProps);
useEffect(() => {
setPageProps(commonPageNationsProps);
}, [commonPageNationsProps]);
useEffect(() => {
setPageProps(commonPageNationsProps);
} ,[commonPageNationsProps]);
const totalPages = Math.ceil(pageProps.total / pageProps.pageSize);
const pagesPerGroup = 10;
const currentGroup = Math.floor((pageProps.cur - 1) / pagesPerGroup);
const startPage = currentGroup * pagesPerGroup + 1;
const endPage = Math.min(startPage + pagesPerGroup - 1, totalPages);
const handlePageChange = (page: number) => {
pageProps.onPageChange(page);
};
return(
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" />
</PaginationItem>
return (
<Pagination>
<PaginationContent>
{/* 이전 그룹으로 이동 */}
<PaginationItem>
<PaginationPrevious
href="#"
onClick={() => {
if (startPage > 1) handlePageChange(startPage - 1);
}}
/>
</PaginationItem>
{/* 현재 그룹 페이지들 표시 */}
{
Array.from({length:Math.ceil(commonPageNationsProps.total/commonPageNationsProps.pageSize)},(_,i)=>i+1).map((el,index)=>(
<PaginationItem key={index}>
<PaginationLink href= '#'>{el}</PaginationLink>
</PaginationItem>
))
}
<PaginationItem>
<PaginationNext href="#" />
Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i).map((pageNum) => (
<PaginationItem key={pageNum}>
<PaginationLink
href="#"
isActive={pageNum === pageProps.cur}
onClick={() => handlePageChange(pageNum)}
>
{pageNum}
</PaginationLink>
</PaginationItem>
</PaginationContent>
</Pagination>
);
))
}
{/* 다음 그룹으로 이동 */}
<PaginationItem>
<PaginationNext
href="#"
onClick={() => {
if (endPage < totalPages) handlePageChange(endPage + 1);
}}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
);
}
@@ -1,93 +1,8 @@
import presetConfig from "../../../tailwind.config.preset";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class", "[data-mode='dark']"],
content: ["./src/**/*.{ts,tsx,js,jsx}", "./.storybook/**/*.{ts,tsx,js,jsx}"],
theme: {
extend: {
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
card: {
DEFAULT: "var(--card)",
foreground: "var(--card-foreground)",
},
popover: {
DEFAULT: "var(--popover)",
foreground: "var(--popover-foreground)",
},
primary: {
DEFAULT: "var(--primary)",
foreground: "var(--primary-foreground)",
},
secondary: {
DEFAULT: "var(--secondary)",
foreground: "var(--secondary-foreground)",
},
muted: {
DEFAULT: "var(--muted)",
foreground: "var(--muted-foreground)",
},
accent: {
DEFAULT: "var(--accent)",
foreground: "var(--accent-foreground)",
},
destructive: {
DEFAULT: "var(--destructive)",
foreground: "var(--destructive-foreground)",
},
border: "var(--border)",
input: "var(--input)",
label: "var(--label)",
ring: "var(--ring)",
chart: {
1: "var(--chart-1)",
2: "var(--chart-2)",
3: "var(--chart-3)",
4: "var(--chart-4)",
5: "var(--chart-5)",
},
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
"spin-parent": {
from: { transform: "translate(-50%,-50%) rotate(0deg)" },
to: { transform: "translate(-50%,-50%) rotate(360deg)" },
},
"spin-child": {
from: { transform: "translate(0,0) rotate(0deg)" },
to: { transform: "translate(0,0) rotate(360deg)" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"spin-parent": "spin-parent 6s linear infinite",
"spin-child": "spin-child 1s ease-in-out infinite",
},
backgroundImage: {
"gradient-blue": "linear-gradient(.25turn, #2094fa, #00e5dd)",
"gradient-blue-hover":
"linear-gradient(.25turn, #2298ff, #01dcda, #00e8b4)",
"gradient-green": "linear-gradient(.25turn, #00bad0, #00eb6f)",
"gradient-green-hover": "linear-gradient(.25turn, #00bad0, #12fe81)",
"gradient-red": "linear-gradient(.25turn, #ff487c, #ffc425)",
"gradient-red-hover": "linear-gradient(.25turn, #ff487c, #ffea3a)",
"gradient-purple": "linear-gradient(.25turn, #be30d7, #ff9264)",
"gradient-purple-hover": "linear-gradient(.25turn, #be30d7, #ffaf5a)",
},
},
},
presets: [presetConfig],
content: ["./src/**/*.{ts,tsx,js,jsx}"],
plugins: [require("tailwindcss-animate")],
};