Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,15 @@
--border: #BDE3EC;
--input: #FFFFFF;
--ring: #5BBECE;
--chart-1: #c2622d;
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: #3a9e94;
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: #2e5e8e;
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: #d4b84a;
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: #c8a030;
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: #EEF7F9;
--sidebar-foreground: #1A4A5A;
Expand All @@ -111,6 +116,30 @@
--dark-body: #1A4A5A;
--dark-description: #3A8A9A;
--dark-border: #BDE3EC;

/* 유체 타이포그래피: clamp(최솟값, 뷰포트 기반 선호값, 최댓값) */
--fs-xs: clamp(0.6875rem, 0.0625vw + 0.675rem, 0.75rem);
--fs-sm: clamp(0.8125rem, 0.0625vw + 0.8rem, 0.875rem);
--fs-base: clamp(0.875rem, 0.125vw + 0.85rem, 1rem);
--fs-lg: clamp(1rem, 0.125vw + 0.975rem, 1.125rem);
--fs-xl: clamp(1.125rem, 0.125vw + 1.1rem, 1.25rem);
--fs-2xl: clamp(1.25rem, 0.25vw + 1.2rem, 1.5rem);

/* 유체 간격: 동일 공식으로 저해상도↔고해상도 자연스럽게 전환 */
--sp-2: clamp(0.375rem, 0.125vw + 0.35rem, 0.5rem);
--sp-3: clamp(0.625rem, 0.125vw + 0.6rem, 0.75rem);
--sp-4: clamp(0.875rem, 0.125vw + 0.85rem, 1rem);
--sp-6: clamp(1.25rem, 0.25vw + 1.2rem, 1.5rem);
--sp-8: clamp(1.5rem, 0.5vw + 1.4rem, 2rem);

/* 컴포넌트별 유체 값 */
--fluid-gap-kanban: clamp(0.75rem, 0.75vw + 0.6rem, 1.5rem);
--fluid-px-outer: clamp(0.5rem, 1.25vw, 1rem);
--fluid-py-outer: clamp(0.5rem, 0.625vw + 0.25rem, 0.75rem);
--fluid-modal-py: clamp(1.5rem, 1.25vw + 1.25rem, 2.25rem);
--fluid-modal-px: clamp(1.25rem, 0.85vw + 1.05rem, 1.75rem);
--fluid-modal-min-h: clamp(12rem, 40vh, 17rem);
--fluid-textarea-p: clamp(0.75rem, 0.4vw + 0.675rem, 1rem);
}

.dark {
Expand All @@ -133,10 +162,15 @@
--border: #2A6070;
--input: #1A4A5A;
--ring: #6DD5E8;
--chart-1: #3a6fd8;
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: #4db89a;
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: #c8a030;
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: #9b4fd8;
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: #d84a3a;
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: #0D2530;
--sidebar-foreground: #E8F5F7;
Expand All @@ -154,6 +188,14 @@
}

@layer base {
html {
/* Windows 고정 스크롤바(15-17px)로 인한 레이아웃 밀림 방지 */
scrollbar-gutter: stable;
}
*, *::before, *::after {
/* OS별 box model 차이 통일 */
box-sizing: border-box;
}
* {
@apply border-border outline-ring/50;
}
Expand All @@ -162,6 +204,11 @@
}
}

/* 스크롤바 공간 예약 — Mac/Windows 레이아웃 일치 */
.scrollbar-gutter-stable {
scrollbar-gutter: stable;
}

/* ==========================================================================
React Calendar (react-calendar)
========================================================================== */
Expand Down Expand Up @@ -633,7 +680,6 @@
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
scrollbar-color: var(--color-gray-300) transparent;
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function RootLayout({
<AuthProviders>
<div className="h-full flex flex-col">
<Header />
<div className="flex-1 overflow-auto">{children}</div>
<div className="flex-1 min-w-0 overflow-auto scrollbar-gutter-stable">{children}</div>
<Toaster />
</div>
</AuthProviders>
Expand Down
6 changes: 3 additions & 3 deletions src/components/features/kanban/KanbanBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -454,10 +454,10 @@ function ColumnGrid({
isDragging: boolean;
}) {
return (
<div className="flex-1 min-h-0 flex flex-col px-2 sm:px-4 py-2 sm:py-3">
<div className="flex-1 min-h-0 flex flex-col px-(--fluid-px-outer) py-(--fluid-py-outer)">
<div className="h-full flex flex-col rounded-lg border border-border overflow-hidden bg-[#F0F4F5] dark:bg-muted/20">
<div className="flex-1 min-h-0 overflow-x-auto overflow-y-auto">
<div className="flex gap-4 h-full justify-start sm:justify-center min-w-fit p-4">
<div className="flex-1 min-h-0 overflow-x-auto overflow-y-auto scrollbar-gutter-stable">
<div className="flex gap-(--fluid-gap-kanban) h-full justify-start sm:justify-center min-w-fit p-(--fluid-gap-kanban) 2xl:max-w-[1680px] 2xl:mx-auto">
{KANBAN_COLUMNS.map((column) => (
<KanbanColumn
key={column.id}
Expand Down
4 changes: 2 additions & 2 deletions src/components/features/kanban/MemoView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@ const MemoView = ({ projectId }: MemoFormProps) => {
placeholder="메모를 입력하세요..."
disabled={loadingMemos}
rows={3}
className="w-full p-3 sm:p-4 pr-8 sm:pr-10 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900/50 rounded-xl resize-none text-sm input-focus-style transition-all placeholder:text-gray-400 dark:placeholder:text-gray-500 text-gray-900 dark:text-gray-100 disabled:opacity-50"
className="w-full pt-(--fluid-textarea-p) pb-(--fluid-textarea-p) pl-(--fluid-textarea-p) pr-8 sm:pr-10 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900/50 rounded-xl resize-none text-sm input-focus-style transition-all placeholder:text-gray-400 dark:placeholder:text-gray-500 text-gray-900 dark:text-gray-100 disabled:opacity-50"
/>
{newMemo && (
<button
Expand Down Expand Up @@ -801,7 +801,7 @@ const MemoView = ({ projectId }: MemoFormProps) => {

{/* 작성자 */}
<div className="flex justify-end">
<span className="inline-flex items-center gap-1.5 px-2 py-1 bg-white/60 dark:bg-gray-800/60 backdrop-blur-sm rounded-md text-xs text-gray-600 dark:text-gray-400">
<span className="inline-flex items-center gap-1.5 px-2 py-1 bg-white/80 dark:bg-gray-800/80 supports-backdrop-filter:bg-white/60 supports-backdrop-filter:dark:bg-gray-800/60 supports-backdrop-filter:backdrop-blur-sm rounded-md text-xs text-gray-600 dark:text-gray-400">
<svg
className="w-3 h-3"
fill="currentColor"
Expand Down
153 changes: 72 additions & 81 deletions src/components/ui/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@ import clsx from "clsx";
import Button from "@/components/ui/Button";
import { Icon } from "@/components/shared/Icon";
import { useEffect } from "react";
import { createPortal } from "react-dom";
import { ModalProps } from "@/types/modal";
import { modalConfigs } from "@/config/modalConfigs";

const modalClasses = clsx(
"fixed left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 z-40",
"w-full h-full",
"flex items-center"
);

const modalInnerClasses = clsx(
"relative m-auto w-xl min-h-68 py-9 px-7",
"relative w-[calc(100%-2rem)] max-w-xl min-h-(--fluid-modal-min-h)",
"py-(--fluid-modal-py) px-(--fluid-modal-px)",
"flex flex-col items-center justify-center",
"border border-gray-100 dark:border-gray-700 rounded-2xl shadow-sm bg-white dark:bg-gray-800"
);
Expand All @@ -39,24 +35,20 @@ export default function Modal({
const config: any = type
? modalConfigs[type as keyof typeof modalConfigs]
: { title: undefined, description: undefined, warning: undefined };
// prop으로 전달받은 title, description이 있다면 사용하고, 없으면 config 값 사용
const finalTitle = title ?? config.title;
const finalDescription = description ?? config.description;
const finalWarning = warning ?? config.warning;

// 성공 타입은 5초 자동 닫힘
useEffect(() => {
if (type === "success" || type === "deleteSuccess") {
const timer = setTimeout(() => {
onClose();
}, 5000);

return () => clearTimeout(timer);
}
return undefined;
}, [type, onClose]);

// body 스크롤 방지
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
Expand All @@ -66,14 +58,12 @@ export default function Modal({
};
}, [isOpen]);

// ESC 키로 모달 닫기
useEffect(() => {
const handleEscKey = (e: KeyboardEvent) => {
if (e.key === "Escape" && isOpen) {
onClose();
}
};

window.addEventListener("keydown", handleEscKey);
return () => window.removeEventListener("keydown", handleEscKey);
}, [isOpen, onClose]);
Expand All @@ -82,84 +72,85 @@ export default function Modal({
return null;
}

return (
<div className={modalClasses}>
return createPortal(
<>
{/* backdrop */}
<div
className="fixed inset-0 bg-black/50 dark:bg-black/70"
className="fixed inset-0 z-200 bg-black/50 dark:bg-black/70"
onClick={onClose}
/>
<div className={modalInnerClasses}>
{/* 모달 아이콘 */}
{config?.icon && (
<div className={modalIconClasses}>
<Icon
type={config.icon}
className={`${config.iconColor}`}
size={config.iconSize}
/>
</div>
)}
{/* 닫기 버튼 */}
<button
onClick={onClose}
className="absolute top-3 right-3 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
>
<Icon type="x" />
</button>

{/* 텍스트 또는 커스텀 컨텐츠 */}
<div className="text-center w-full">
{children ? (
children
) : (
<>
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{finalTitle}
</h2>
<p className="text-base font-medium mt-2 text-gray-700 dark:text-gray-300">
{finalDescription}
</p>
{/* modal */}
<div className="fixed inset-0 z-201 flex items-center justify-center">
<div className={modalInnerClasses}>
{config?.icon && (
<div className={modalIconClasses}>
<Icon
type={config.icon}
className={`${config.iconColor}`}
size={config.iconSize}
/>
</div>
)}
<button
onClick={onClose}
className="absolute top-3 right-3 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
>
<Icon type="x" />
</button>

{finalWarning && (
<p className="text-sm font-semibold mt-5 text-red-500 dark:text-red-400">
{finalWarning}
<div className="text-center w-full">
{children ? (
children
) : (
<>
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{finalTitle}
</h2>
<p className="text-base font-medium mt-2 text-gray-700 dark:text-gray-300">
{finalDescription}
</p>
)}

{config?.info && (
<p
className={`text-sm font-medium mt-5 ${
config.infoColor || "text-gray-600 dark:text-gray-400"
}`}
{finalWarning && (
<p className="text-sm font-semibold mt-5 text-red-500 dark:text-red-400">
{finalWarning}
</p>
)}

{config?.info && (
<p
className={`text-sm font-medium mt-5 ${
config.infoColor || "text-gray-600 dark:text-gray-400"
}`}
>
{config.info}
</p>
)}
</>
)}
</div>

{(config?.buttonCancelText || config?.buttonConfirmText) && (
<div className="flex gap-3 justify-center mt-6">
{config?.buttonCancelText && (
<Button btnType="basic" variant="basic" onClick={onClose}>
{config.buttonCancelText}
</Button>
)}
{config?.buttonConfirmText && (
<Button
btnType="basic"
variant="warning"
onClick={onConfirm}
disabled={buttonDisabled ?? config.buttonDisabled}
>
{config.info}
</p>
{config.buttonConfirmText}
</Button>
)}
</>
</div>
)}
</div>

{/* 버튼 */}
{(config?.buttonCancelText || config?.buttonConfirmText) && (
<div className="flex gap-3 justify-center mt-6">
{config?.buttonCancelText && (
<Button btnType="basic" variant="basic" onClick={onClose}>
{config.buttonCancelText}
</Button>
)}
{config?.buttonConfirmText && (
<Button
btnType="basic"
variant="warning"
onClick={onConfirm}
disabled={buttonDisabled ?? config.buttonDisabled}
>
{config.buttonConfirmText}
</Button>
)}
</div>
)}
</div>
</div>
</>,
document.body
);
}
22 changes: 20 additions & 2 deletions src/components/ui/shadcn/Textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,32 @@ import * as React from "react";

import { cn } from "@/lib/utils/utils";

function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
function Textarea({ className, onChange, ...props }: React.ComponentProps<"textarea">) {
const ref = React.useRef<HTMLTextAreaElement>(null);

const autoResize = (el: HTMLTextAreaElement) => {
el.style.height = "auto";
el.style.height = `${el.scrollHeight}px`;
};

React.useEffect(() => {
if (ref.current) autoResize(ref.current);
}, [props.value, props.defaultValue]);

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
autoResize(e.target);
onChange?.(e);
};

return (
<textarea
ref={ref}
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-3 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex min-h-16 w-full rounded-md border bg-transparent px-3 py-3 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm resize-none overflow-hidden",
className
)}
onChange={handleChange}
{...props}
/>
);
Expand Down
2 changes: 1 addition & 1 deletion src/features/project/ui/ProjectCreationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export function ProjectCreationModal({ open, onOpenChange }: ProjectCreationModa
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/40 backdrop-blur-sm z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
<Dialog.Overlay className="fixed inset-0 bg-black/50 supports-backdrop-filter:bg-black/40 supports-backdrop-filter:backdrop-blur-sm z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
<Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-card rounded-[14px] shadow-[0px_1px_3px_0px_rgba(0,0,0,0.1),0px_1px_2px_-1px_rgba(0,0,0,0.1)] border border-border z-50 w-[calc(100%-2rem)] sm:w-full max-w-[560px] max-h-[90vh] overflow-y-auto">
<Dialog.Title className="sr-only">프로젝트 생성</Dialog.Title>
{/* 헤더 */}
Expand Down
Loading