Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
10 changes: 7 additions & 3 deletions src/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@ interface LayoutProps {

export function Layout({ left, center, right }: LayoutProps) {
return (
<div className="min-h-screen bg-gray-100">
<div className="h-screen overflow-hidden bg-gray-100">
<header className="fixed top-0 right-0 left-0 z-50 flex h-15 items-center justify-between border-b border-gray-200 bg-white px-18">
<div className="flex items-center gap-6">{left ?? <Logo />}</div>
<div className="absolute left-1/2 -translate-x-1/2">{center}</div>
<div className="flex items-center gap-8">{right}</div>
</header>
<main className="pt-15">
<Outlet />

<main className="mt-15 h-[calc(100vh-3.75rem)] overflow-hidden">
{/* Outlet이 들어가는 영역(= 네 컨텐츠)이 이제 정확한 높이를 가짐 */}
<div className="h-full">
<Outlet />
</div>
</main>
</div>
);
Expand Down
65 changes: 48 additions & 17 deletions src/components/script-box/ScriptBox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';

import clsx from 'clsx';

Expand All @@ -7,31 +7,62 @@ import ScriptBoxHeader from './ScriptBoxHeader';

interface ScriptBoxProps {
slideTitle?: string;
/**
* [기존] ScriptBox는 내부에서만 접힘 상태를 알고 있었고,
* 슬라이드(상단 콘텐츠)는 ScriptBox 상태에 맞춰 움직일 수 없었음.
* [현재] 부모가 접힘 상태를 알 수 있어야 "슬라이드를 살짝 내려오는" UI 연동이 가능함.
*/
onCollapsedChange?: (collapsed: boolean) => void;
}

export default function ScriptBox({ slideTitle = '슬라이드 1' }: ScriptBoxProps) {
export default function ScriptBox({
slideTitle = '슬라이드 1',
onCollapsedChange,
}: ScriptBoxProps) {
// ScriptBox 접힌 상태인지 체크
const [isCollapsed, setIsCollapsed] = useState(false);

const handleToggleCollapse = () => {
setIsCollapsed((prev) => !prev);
};

/**
* 접힘 상태가 바뀔 때마다 부모에게 알려줌
* - 부모는 이 상태를 받아 슬라이드 영역에 transform/padding 등으로 반응하게 됨
*/
useEffect(() => {
onCollapsedChange?.(isCollapsed);
}, [isCollapsed, onCollapsedChange]);

return (
<div
className={clsx(
'fixed inset-x-0 bottom-0 z-30',
'mx-auto w-full rounded-t-lg bg-white',
'transition-transform duration-300 ease-out',
'h-80',
isCollapsed ? 'translate-y-[calc(100%-2.5rem)]' : 'translate-y-0',
)}
>
<ScriptBoxHeader
slideTitle={slideTitle}
isCollapsed={isCollapsed}
onToggleCollapse={handleToggleCollapse}
/>
<ScriptBoxContent />
<div className={clsx('w-full rounded-t-lg bg-white shadow-sm', isCollapsed ? 'h-10' : 'h-80')}>
{/* 헤더는 그대로 */}
<div className="h-12 relative">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

헤더를 감싸는 div의 높이가 h-12(48px)로 설정되어 있습니다. 하지만 ScriptBox가 접혔을 때 부모 div의 높이는 h-10(40px)이고, ScriptBoxHeader 컴포넌트 자체의 높이도 h-10입니다. 이로 인해 자식 요소가 부모 요소보다 커져 레이아웃이 깨질 수 있습니다. h-10으로 수정하여 높이를 일치시키는 것이 좋아 보입니다.

Suggested change
<div className="h-12 relative">
<div className="h-10 relative">

<ScriptBoxHeader
slideTitle={slideTitle}
isCollapsed={isCollapsed}
onToggleCollapse={handleToggleCollapse}
/>
</div>

{/**
* 본문 영역만 따로 감싸서 접힘 애니메이션 처리
*
* 왜 wrapper를 하나 더 뒀나?
* - 헤더는 popover 때문에 잘리면 안 됨 → overflow-hidden을 헤더가 아닌 "본문 wrapper"에만 적용
* - 접힘 애니메이션은 "height 변화"로 처리하는 게 가장 예측 가능함
*
* [기존] translate로 박스를 숨김 → 공간 계산, popover, 클릭 영역이 꼬일 수 있음
* [현재] content wrapper의 높이를 h-0으로 만들어 진짜로 본문이 없어지게 함(접힌 상태라면 아예 랜더 자체 하지 않음)
*/}
<div
className={clsx(
'overflow-hidden transition-[height] duration-300 ease-out',
isCollapsed ? 'h-0' : 'h-[280px]',
)}
>
{!isCollapsed && <ScriptBoxContent />}
</div>
</div>
);
}
3 changes: 2 additions & 1 deletion src/components/script-box/ScriptBoxContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ export default function ScriptBoxContent() {
const [script, setScript] = useState('');

return (
<div className="h-[calc(100%-2.5rem)] overflow-y-auto border-b border-gray-200 bg-white px-4 pb-6 pt-3">
// ScriptBox 전체 높이에서 헤더만큼 뺀 영역을 그대로 사용
<div className="h-full overflow-y-auto bg-white px-4 py-3">
<textarea
value={script}
onChange={(e) => setScript(e.target.value)}
Expand Down
196 changes: 191 additions & 5 deletions src/pages/SlidePage.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,211 @@
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';

import clsx from 'clsx';

import { ScriptBox } from '@/components/script-box';
import { setLastSlideId } from '@/constants/navigation';

export default function SlidePage() {
// url에 입력된 프로젝트, 슬라이드id 읽기
const { projectId, slideId } = useParams<{
projectId: string;
slideId: string;
}>();

/**
*
* 목적:
* - 탭 이동(영상/인사이트 → 슬라이드) 후 다시 돌아왔을 때
* → 마지막으로 보던 슬라이드로 복원하기 위함
*/
useEffect(() => {
if (projectId && slideId) {
setLastSlideId(projectId, slideId);
}
}, [projectId, slideId]);

/**
* ScriptBox가 접혔는지 여부
* - ScriptBox 내부 상태를 부모(SlidePage)에서 받아서
* - 슬라이드 영역을 독립적으로 이동시키는 데 사용
*/
const [isScriptCollapsed, setIsScriptCollapsed] = useState(false);

/**
* 임시 슬라이드 데이터
* - slideId 기준으로 현재 슬라이드 결정
* - 왼쪽 썸네일 리스트 및 중앙 슬라이드에 사용
* - 추후 서버에서 받아온 데이터로 대체
*/
const slides = [
{
id: '1',
title: '도입',
thumb: 'https://via.placeholder.com/160x90?text=1',
content: '이번 프로젝트에서 다루고자 하는 주제와 전체 발표 흐름을 간단히 소개합니다.',
},
{
id: '2',
title: '문제 정의',
thumb: 'https://via.placeholder.com/160x90?text=2',
content: '현재 사용자가 겪고 있는 불편함과 기존 방식의 한계를 정리합니다.',
},
{
id: '3',
title: '문제 분석',
thumb: 'https://via.placeholder.com/160x90?text=3',
content: '문제가 발생하는 원인을 기능·구조·사용 흐름 관점에서 분석합니다.',
},
{
id: '4',
title: '해결 목표',
thumb: 'https://via.placeholder.com/160x90?text=4',
content: '이번 개선을 통해 달성하고자 하는 핵심 목표와 방향성을 정의합니다.',
},
{
id: '5',
title: '해결 방안',
thumb: 'https://via.placeholder.com/160x90?text=5',
content: '문제 해결을 위해 제안하는 주요 기능과 UI/UX 전략을 설명합니다.',
},
{
id: '6',
title: '기능 구성',
thumb: 'https://via.placeholder.com/160x90?text=6',
content: '슬라이드, 스크립트 박스 등 핵심 기능들의 구성과 역할을 소개합니다.',
},
{
id: '7',
title: '화면 흐름',
thumb: 'https://via.placeholder.com/160x90?text=7',
content: '사용자가 화면을 어떻게 탐색하고 상호작용하는지 흐름 중심으로 설명합니다.',
},
{
id: '8',
title: '기술적 구현',
thumb: 'https://via.placeholder.com/160x90?text=8',
content: '레이아웃 분리, 상태 관리 등 구현 과정에서의 핵심 기술적 포인트를 다룹니다.',
},
{
id: '9',
title: '기대 효과',
thumb: 'https://via.placeholder.com/160x90?text=9',
content: '이번 개선으로 사용자 경험과 개발 구조 측면에서 기대되는 효과를 정리합니다.',
},
{
id: '10',
title: '결론',
thumb: 'https://via.placeholder.com/160x90?text=10',
content: '전체 내용을 요약하고, 향후 확장 또는 개선 방향을 제안하며 마무리합니다.',
},
];

// 현재 선택 중인 슬라이드 중앙 배치
const currentSlide = slides.find((s) => s.id === slideId) ?? slides[0];
const basePath = projectId ? `/${projectId}` : '';

return (
<div role="tabpanel" id="tabpanel-slide" aria-labelledby="tab-slide" className="p-8">
<h1 className="text-body-m-bold">슬라이드 {slideId}</h1>
<ScriptBox />
<div className="h-full bg-gray-100">
{/* 양쪽 다 바닥까지 꽉: h-full */}
<div className="flex h-full gap-8 px-20 py-0">
{/* ================= LEFT ================= */}
{/* 슬라이드 썸네일 영역
- 여기만 스크롤 가능
- 슬라이드 이동은 URL 기반으로 처리 */}
<aside className="w-80 min-w-80 h-full overflow-y-auto">
<div className="flex flex-col gap-5 pr-10">
{slides.map((s, idx) => {
const isActive = s.id === currentSlide.id;

return (
<div
key={s.id}
className={clsx(
'flex items-start gap-3 rounded-xl border p-2 bg-white',
isActive ? 'border-main' : 'border-gray-200',
)}
>
<div className="w-6 pt-2 text-right text-sm font-semibold text-gray-700 select-none">
{idx + 1}
</div>
{/* 슬라이드 이동 링크 */}
<Link
to={`${basePath}/slide/${s.id}`}
aria-current={isActive ? 'true' : undefined}
className={clsx(
'block w-full h-40 rounded-lg overflow-hidden',
'focus:outline-none focus:ring-2 focus:ring-main',
)}
>
<div
className={clsx(
'h-full w-full rounded-lg transition',
isActive ? 'bg-gray-200' : 'bg-gray-100 hover:bg-gray-200',
)}
/>
</Link>
</div>
);
})}
</div>
</aside>

{/* ================= RIGHT ================= */}
{/* 핵심 영역:
- 슬라이드와 ScriptBox를 완전히 분리된 레이어로 구성
- ScriptBox는 항상 하단 고정
- 슬라이드는 ScriptBox 상태에 따라 독립적으로 이동 */}
<main className="flex-1 h-full overflow-hidden">
<div className="relative h-full">
{/* 슬라이드 레이어
- absolute로 전체 영역을 덮음
- ScriptBox 높이만큼 paddingBottom으로 안전 영역 확보 */}
<div
className="absolute left-0 right-0 top-0 bottom-0 flex justify-center"
style={{
paddingBottom: isScriptCollapsed ? 40 : 100,

paddingTop: 50,
}}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

스크립트 박스가 펼쳐졌을 때(isScriptCollapsedfalse) paddingBottom100으로 설정되어 있습니다. ScriptBox의 실제 높이는 320px이므로, 이 값은 슬라이드 하단이 ScriptBox에 의해 가려지게 만듭니다. 접혔을 때의 높이 40px와 일관되게, 펼쳐졌을 때의 높이 320pxpaddingBottom으로 설정하여 레이아웃이 깨지지 않도록 하는 것을 제안합니다.

Suggested change
style={{
paddingBottom: isScriptCollapsed ? 40 : 100,
paddingTop: 50,
}}
style={{
paddingBottom: isScriptCollapsed ? 40 : 320,
paddingTop: 50,
}}

>
{/* 슬라이드 컨테이너
- ScriptBox 접힘 상태에 따라 translateY로 "조금만" 이동
- 레이아웃 계산이 아닌 명시적 이동을 사용 */}
<div
className="w-[2200px] flex flex-col items-center"
style={{
// ✅ 여기서 '독립적으로' 내려가게 만듦 (원하는 만큼만)
transform: `translateY(${isScriptCollapsed ? 120 : 0}px)`,
transition: 'transform 300ms ease-out',
}}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

레이아웃 및 애니메이션과 관련된 여러 숫자 값들(padding, transform, 너비/높이 등)이 코드 곳곳에 "매직 넘버"로 하드코딩되어 있습니다. (예: 40, 50, 120, 2200, 1238) 이는 코드의 가독성을 떨어뜨리고, 나중에 값을 일관성 있게 변경하기 어렵게 만듭니다.

이러한 값들을 컴포넌트 상단에 의미있는 이름의 상수로 선언하여 사용하면 코드의 의도를 더 명확히 하고 유지보수성을 크게 향상시킬 수 있습니다.

예시:

const SCRIPT_BOX_COLLAPSED_HEIGHT = 40;
const SCRIPT_BOX_EXPANDED_HEIGHT = 320; // 이전 댓글에서 제안한 수정값
const SLIDE_AREA_TOP_PADDING = 50;
const SLIDE_TRANSLATE_Y_ON_COLLAPSE = 120;
const SLIDE_WIDTH = 2200;
const SLIDE_HEIGHT = 1238;

>
{/* 실제 슬라이드 */}
<div className="w-[2200px] h-[1238px] rounded-2xl bg-gray-200 shadow-sm relative">
<span className="text-2xl font-bold text-gray-800 absolute top-10 left-8">
{currentSlide.title}
</span>
<span className="text-base text-gray-600 absolute bottom-6 left-8">
{currentSlide.content}
</span>
</div>
</div>
</div>

{/* ScriptBox 레이어
- 오른쪽 컬럼 기준 absolute bottom 고정
- 슬라이드와 동일한 폭을 사용 */}
<div className="absolute bottom-0 left-0 right-0 flex justify-center">
<div className="w-[2200px]">
<ScriptBox
slideTitle={`슬라이드 ${slideId ?? currentSlide.id}`}
onCollapsedChange={setIsScriptCollapsed}
/>
</div>
</div>
</div>
</main>
</div>
</div>
);
}