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
15 changes: 12 additions & 3 deletions src/app/project/_components/ProjectItem.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { faker } from "@faker-js/faker";
import ProjectItemComponent from "@/app/project/_components/ProjectItem";

const meta = {
title: "ProjectItem",
title: "Project/ProjectItem",
component: ProjectItemComponent,
parameters: {
layout: "centered",
Expand All @@ -24,13 +24,22 @@ export const Default: Story = {
args: {
project: {
title: faker.lorem.sentence(),
description: faker.lorem.sentence(),
thumbnail: faker.image.url(),
subTitle: faker.lorem.sentence(),
listThumbnail: faker.image.url(),
popupThumbnail: faker.image.url(),
typeofApp: "web",
semester: 11,
isNew: true,
index: 1,
id: 1,
description: faker.lorem.sentence(),
keyFeatures: [faker.lorem.sentence(), faker.lorem.sentence()],
team: [
{
position: faker.lorem.word(),
memberNames: [faker.person.fullName()],
},
],
},
},
render: function Render(args) {
Expand Down
10 changes: 5 additions & 5 deletions src/app/project/_components/ProjectItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Project } from "@/app/project/_types/project";
import { Project, AppType } from "@/app/project/_types/project";
import Image from "next/image";
import { ComponentPropsWithoutRef } from "react";
import { cn } from "@/utils/cn";
Expand All @@ -7,21 +7,21 @@ interface ProjectItemProps extends ComponentPropsWithoutRef<"button"> {
project: Project;
}

const TYPE_OF_APP_MAP: Record<Project["typeofApp"], string> = {
const TYPE_OF_APP_MAP: Record<AppType, string> = {
iOS: "iOS APP",
Android: "ANDROID APP",
web: "WEB",
} as const;

export default function ProjectItem({ project, ...props }: ProjectItemProps) {
const { typeofApp, index, isNew, semester, title, description, thumbnail } =
const { typeofApp, index, isNew, semester, title, subTitle, listThumbnail } =
project;

return (
<button {...props} className="flex flex-col gap-8 w-full">
<div className="relative w-[100%] aspect-square">
<Image
src={thumbnail}
src={listThumbnail}
alt={title}
layout="fill"
objectFit="cover"
Expand Down Expand Up @@ -50,7 +50,7 @@ export default function ProjectItem({ project, ...props }: ProjectItemProps) {
{title}
</div>
<div className="text-text-secondary desktop:text-body-2-regular netbook:text-body-3-regular tablet:text-body-2-regular mobile:text-body-3-regular text-left line-clamp-2 whitespace-pre-line overflow-hidden">
{description}
{subTitle}
</div>
</div>
<div className="flex gap-4">
Expand Down
89 changes: 89 additions & 0 deletions src/app/project/_components/ProjectItemPopup.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { faker } from "@faker-js/faker/locale/ko";
import ProjectItemPopup from "./ProjectItemPopup";
import TextButton from "@/components/TextButton";
import { Project } from "@/app/project/_types/project";

const meta = {
title: "Project/ProjectItemPopup",
component: ProjectItemPopup,
parameters: {
layout: "fullscreen",
},
} satisfies Meta<typeof ProjectItemPopup>;

export default meta;
type Story = StoryObj<typeof ProjectItemPopup>;

const mockProject: Project = {
id: faker.number.int(),
title: faker.company.name(),
subTitle: faker.company.catchPhrase(),
listThumbnail: faker.image.url({ width: 800, height: 450 }),
popupThumbnail: faker.image.url({ width: 1024, height: 576 }),
typeofApp: "web",
semester: faker.number.int({ min: 1, max: 10 }),
isNew: faker.datatype.boolean(),
index: faker.number.int({ min: 1, max: 10 }),
description: faker.lorem.paragraphs(3),
keyFeatures: Array.from(
{ length: faker.number.int({ min: 3, max: 6 }) },
() => faker.lorem.sentence()
),
team: [
{
position: "프론트엔드",
memberNames: Array.from(
{ length: faker.number.int({ min: 2, max: 4 }) },
() => faker.person.fullName()
),
},
{
position: "백엔드",
memberNames: Array.from(
{ length: faker.number.int({ min: 2, max: 4 }) },
() => faker.person.fullName()
),
},
{
position: "디자인",
memberNames: Array.from(
{ length: faker.number.int({ min: 1, max: 3 }) },
() => faker.person.fullName()
),
},
{
position: "PM",
memberNames: [faker.person.fullName()],
},
],
};

const ProjectItemPopupWithControls = ({
project,
}: {
project: typeof mockProject;
}) => {
const [isOpen, setIsOpen] = useState(false);

return (
<div className="p-24">
<TextButton onClick={() => setIsOpen(true)}>
프로젝트 상세 보기
</TextButton>
<ProjectItemPopup
project={project}
isOpen={isOpen}
onClose={() => setIsOpen(false)}
/>
</div>
);
};

export const Default: Story = {
render: (args) => <ProjectItemPopupWithControls {...args} />,
args: {
project: mockProject,
},
};
154 changes: 154 additions & 0 deletions src/app/project/_components/ProjectItemPopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"use client";

import { useEffect, useCallback } from "react";
import Image from "next/image";
import { Portal } from "@/components/Portal";
import CloseIcon from "@/components/svgs/CloseIcon";
import SolidIconButton from "@/components/IconButton/Solid";
import useMediaQuery from "@/hooks/useMediaQuery";
import { screenMediaQuery } from "@/app/styles/screens";
import type { Project } from "@/app/project/_types/project";
import { Fullscreen } from "@/components/svgs";

interface ProjectItemPopupProps {
project: Project;
isOpen: boolean;
onClose: () => void;
}

const getCircledLetter = (index: number): string => {
// A의 유니코드 시작점: 🅐 (U+1F150)
const circledACode = 0x1f150;
return String.fromCodePoint(circledACode + index);
};

export default function ProjectItemPopup({
project,
isOpen,
onClose,
}: ProjectItemPopupProps) {
const isTablet = useMediaQuery(screenMediaQuery.tablet);

const handleEscape = useCallback(
(event: KeyboardEvent) => {
if (event.key === "Escape") {
onClose();
}
},
[onClose]
);

useEffect(() => {
if (isOpen) {
document.addEventListener("keydown", handleEscape);
document.body.style.overflow = "hidden";
}
return () => {
document.removeEventListener("keydown", handleEscape);
document.body.style.overflow = "unset";
};
}, [isOpen, handleEscape]);

if (!isOpen) return null;

return (
<Portal>
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Overlay */}
<div
className="absolute inset-0 bg-black opacity-[0.8]"
onClick={onClose}
/>

{/* Popup Content */}
<div className="flex justify-center relative z-10 w-full h-full p-[24px] tablet:px-[32px] tablet:py-[36px] netbook:px-[40px] netbook:py-[48px] desktop:px-[48px] desktop:py-[52px]">
<div className="w-full max-w-[1200px] bg-white rounded-[28px] shadow-xl overflow-auto">
{/* Close Button */}
<div className="sticky top-0 flex justify-end p-12 bg-white">
<div className="p-20">
<SolidIconButton
onClick={onClose}
size={isTablet ? "s" : "m"}
className="bg-gray-20"
>
<CloseIcon />
</SolidIconButton>
</div>
</div>

{/* Project Content */}
<div className="desktop:px-[88px] desktop:pb-[112px] netbook:px-[52px] netbook:pb-[80px] tablet:px-[40px] tablet:pb-[52px] mobile:px-[20px] mobile:pb-[24px]">
<div className="flex flex-col desktop:gap-10 netbook:gap-10 tablet:gap-8 mobile:gap-8 desktop:mb-36 netbook:mb-28 tablet:mb-28 mobile:mb-24">
<h2 className="text-text-primary desktop:text-headline-5-bold netbook:text-headline-6-bold tablet:text-headline-7-bold mobile:text-title-2-bold">
{project.title}
</h2>
<h3 className="text-text-primary desktop:text-title-2-medium netbook:text-title-3-medium tablet:text-body-1-medium mobile:text-body-2-medium">
{project.subTitle}
</h3>
</div>
<div className="w-full relative">
<Image
src={project.popupThumbnail}
alt={project.title}
width={792}
height={446}
className="rounded-[12px] w-full"
/>
<div className="absolute top-[12px] right-[12px] desktop:p-12 netbook:p-10 tablet:p-10 mobile:p-8 rounded-[50%] bg-[rgb(12,14,15,0.48)] cursor-pointer">
<Fullscreen className="desktop:w-24 desktop:h-24 netbook:w-20 netbook:h-20 tablet:w-20 tablet:h-20 mobile:w-16 mobile:h-16" />
</div>
</div>
<div className="desktop:pt-40 netbook:pt-32 tablet:pt-32 mobile:pt-32">
<div className="flex flex-col desktop:gap-16 netbook:gap-12 tablet:gap-12 mobile:gap-12">
<h4 className="text-text-primary desktop:text-title-2-bold netbook:text-title-3-bold tablet:text-title-3-bold mobile:text-body-1-bold">
Service Description
</h4>
<p className="text-text-secondary whitespace-pre-wrap desktop:text-body-2-medium netbook:text-body-3-medium tablet:text-body-3-medium mobile:text-body-3-medium">
{project.description}
</p>
</div>

<div className="flex flex-col desktop:gap-16 gap-12 desktop:pt-40 desktop:pb-60 netbook:pt-32 netbook:pb-48 tablet:pt-32 tablet:pb-48 mobile:pt-32 mobile:pb-36">
<h4 className="text-text-primary desktop:text-title-2-bold netbook:text-title-3-bold tablet:text-title-3-bold mobile:text-body-1-bold">
Key Features
</h4>
<ul className="flex flex-col gap-8">
{project.keyFeatures.map((feature, index) => (
<li
key={feature}
className="text-text-secondary text-body-3-medium desktop:text-body-2-medium"
>
<span className="desktop:text-body-2-bold text-body-3-bold">
{getCircledLetter(index)}&nbsp;
</span>
{feature}
</li>
))}
</ul>
</div>

<div className="w-full h-[1px] bg-[#EAEAEA]" />

<div className="flex mobile:flex-wrap desktop:gap-60 netbook:gap-52 tablet:gap-40 mobile:gap-24 desktop:pt-60 netbook:pt-48 tablet:pt-48 mobile:pt-36">
{project.team.map(({ position, memberNames }) => (
<div
key={position}
className="flex flex-col gap-8 min-w-[110px]"
>
<span className="text-text-secondary desktop:text-body-2-regular netbook:text-body-3-regular tablet:text-body-3-regular mobile:text-body-3-regular">
{position}
</span>
<span className="text-text-primary desktop:text-body-2-bold netbook:text-body-3-bold tablet:text-body-3-bold mobile:text-body-3-bold">
{memberNames.join(", ")}
</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
</Portal>
);
}
2 changes: 1 addition & 1 deletion src/app/project/_components/Tab.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
import TabComponent from "@/app/project/_components/Tab";

const meta = {
title: "Tab",
title: "Project/Tab",
component: TabComponent,
parameters: {
layout: "centered",
Expand Down
2 changes: 1 addition & 1 deletion src/app/project/_components/TabList.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
import TabListComponent from "@/app/project/_components/TabList";

const meta = {
title: "TabList",
title: "Project/TabList",
component: TabListComponent,
parameters: {
layout: "centered",
Expand Down
17 changes: 14 additions & 3 deletions src/app/project/_types/project.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import { StaticImport } from "next/dist/shared/lib/get-img-props";

export type Team = {
position: string;
memberNames: string[];
};

export type AppType = "web" | "Android" | "iOS";

export interface Project {
id: number;
title: string;
description: string;
thumbnail: string | StaticImport;
typeofApp: "web" | "Android" | "iOS";
subTitle: string;
listThumbnail: string | StaticImport;
popupThumbnail: string | StaticImport;
typeofApp: AppType;
// 기수
semester: number;
isNew: boolean;
// 짝수이면 border radius 4개 홀수이면 2개
index: number;
description: string;
keyFeatures: string[];
team: Team[];
}
1 change: 1 addition & 0 deletions src/app/styles/spacing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const spacing: CustomThemeConfig["spacing"] = {
"48": "48px",
"52": "52px",
"56": "56px",
"60": "60px",
"64": "64px",
"72": "72px",
"80": "80px",
Expand Down
3 changes: 3 additions & 0 deletions src/assets/icons/fullscreen.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions src/components/svgs/Fullscreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgFullscreen = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24px"
height="24px"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
fill="#fff"
d="M8 3v2H4v4H2V3zM2 21v-6h2v4h4v2zm20 0h-6v-2h4v-4h2zm0-12h-2V5h-4V3h6z"
/>
</svg>
);
export default SvgFullscreen;
Loading