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
4 changes: 4 additions & 0 deletions src/assets/icons/refreshIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/replyIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/smallArrowIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/assets/icons/treshcanIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
457 changes: 457 additions & 0 deletions src/components/ScriptBox.tsx

Large diffs are not rendered by default.

170 changes: 170 additions & 0 deletions src/components/script-box/Opinion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import React from 'react';

import replyIcon from '../../assets/icons/replyIcon.svg';
import treshcanIcon from '../../assets/icons/treshcanIcon.svg';

type OpinionProps = {
opinion: {
value: boolean;
toggle: () => void;
off: () => void;
};

activeReplyIdx: number | null;
setActiveReplyIdx: React.Dispatch<React.SetStateAction<number | null>>;
replyText: string;
// React.SetStateAction : 상태를 바꾸는 set 함수의 타입은 이런 식으로 명시함
setReplyText: React.Dispatch<React.SetStateAction<string>>;
};

const Opinion = ({
opinion,
activeReplyIdx,
setActiveReplyIdx,
replyText,
setReplyText,
}: OpinionProps) => {
return (
<div className="relative inline-block align-top">
{/* (기존 그대로) 의견 버튼 */}
<button
onClick={opinion.toggle}
className={`
h-7 px-2 rounded outline outline-1 outline-offset-[-1px]
inline-flex items-center gap-1
${
opinion.value
? 'outline-indigo-500 text-indigo-500 bg-white'
: 'outline-zinc-200 text-zinc-700 bg-white hover:bg-zinc-50'
}
`}
aria-pressed={opinion.value}
>
<span className="text-sm font-semibold leading-5">의견</span>
<span
className={`${
opinion.value ? 'text-indigo-500' : 'text-gray-500'
} text-sm font-semibold leading-5`}
>
3
</span>
</button>

{/* (기존 그대로) 의견 popover */}
{opinion.value && (
<>
{/* 바깥 클릭 시 닫기 */}
<div className="fixed inset-0 z-40" onClick={opinion.off} />

<div
className="absolute z-50
right-0 bottom-full mb-2
origin-bottom-right
w-[384px] max-w-[90vw]
rounded-lg overflow-hidden bg-white
shadow-[0px_4px_20px_0px_rgba(0,0,0,0.05)]
"
>
<div className="px-4 py-3 bg-white border-b border-zinc-200 flex items-center justify-between">
<div className="text-zinc-700 text-base font-semibold leading-6">의견</div>
</div>

<div className="h-80 overflow-y-auto">
{[0, 1, 2, 3].map((idx) => {
const isMine = idx < 2;
const hasReply = idx === 2;

return (
<div
key={idx}
className={`
${hasReply ? 'pl-14 pr-4' : 'px-4'}
py-3 bg-white
flex items-start gap-3
`}
title={''}
>
<div className="h-20 flex items-start gap-2.5">
<div className="w-8 h-8 bg-zinc-300 rounded-full" />
</div>

<div className="flex-1 pt-1.5 flex flex-col gap-1">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="max-w-48 text-zinc-700 text-sm font-semibold leading-5 line-clamp-1">
{isMine ? '익명 사용자' : '김철수'}
</div>
<div className="text-gray-500 text-xs font-medium leading-4">
{idx === 0 ? '방금 전' : '1시간 전'}
</div>
</div>

{isMine && (
<button
type="button"
className="flex items-center gap-1 text-red-500 text-xs font-semibold leading-4 hover:opacity-80"
>
삭제
<img src={treshcanIcon} className="w-4 h-4" />
</button>
)}
</div>

<div className="text-zinc-700 text-sm font-medium leading-5">
이 부분 설명이 명확해요!
</div>
</div>

<div className="flex items-center gap-4">
{/* ✅ 기존 로직 그대로 유지
- setActiveReplyIdx: 같은 댓글 다시 누르면 닫기(toggle)
- setReplyText: 새로 열 때 입력 초기화 */}
<button
type="button"
className="flex items-center gap-1 text-indigo-500 text-xs font-semibold leading-4 hover:opacity-80"
onClick={() => {
setActiveReplyIdx((prev) => (prev === idx ? null : idx));
setReplyText('');
}}
>
답글
<img src={replyIcon} className="w-4 h-4" />
</button>

{/* 답글 inputBox 렌더링 */}
{activeReplyIdx === idx && (
<div className="mt-2 flex items-center gap-2">
<input
value={replyText}
onChange={(e) => setReplyText(e.target.value)}
placeholder="답글을 입력하세요"
className="flex-1 h-10 px-3 rounded-lg border border-zinc-200 text-sm outline-none focus:border-indigo-500"
/>
{/* 서버 붙기 전: 아무 것도 안 하고 닫기만 */}
<button
type="button"
className="h-10 px-3 rounded-lg bg-zinc-900 text-white text-sm font-semibold hover:opacity-90"
onClick={() => {
setActiveReplyIdx(null);
setReplyText('');
}}
>
등록
</button>
</div>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
</>
)}
</div>
);
};

export default Opinion;
122 changes: 122 additions & 0 deletions src/components/script-box/ScriptBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { useState } from 'react';

import smallArrowIcon from '../../assets/icons/smallArrowIcon.svg';
import { useToggle } from '../../hooks/useToggle';
import Opinion from './Opinion';
import ScriptBoxEmogi from './ScriptBoxEmogi';
import ScriptHistory from './ScriptHistory';
import SlideTitle from './SlideTitle';

const ScriptBox = () => {
// 0. 슬라이드 이름(이름 변경) 버튼
const slideNameChange = useToggle(false);
// 0-1. 현재 슬라이드 이름, 나중에 서버에서 받아온 거로 default 채워야함.
const [slideTitle, setSlideTitle] = useState('슬라이드 1');
// 0-2. 타이틀 저장 버튼(서버 요청)
// const handleSaveSlideTitle = () => {
//
// };

// 1. 이모지버튼 ( 그대로 유지 )
const [isEmogiClick, setEmogiClick] = useState(false);

const handleEmogiClick = () => {
setEmogiClick((prev) => !prev);
};

// 2. 변경기록 버튼
const scriptHistory = useToggle(false);
// 3. 의견 버튼
const opinion = useToggle(false);
// 3-1. 답변 (나중에 피드백id랑 매칭해야됨)
// 어떤 댓글에 답글 입력창이 열려있는지
const [activeReplyIdx, setActiveReplyIdx] = useState<number | null>(null);

// 답글 입력값(일단 1개만)
const [replyText, setReplyText] = useState('');

// 4. 대본섹션 닫기 토글
const scriptBoxDock = useToggle(false);

return (
<>
<div
/* 접힘=true면 전체높이(320) 중 헤더(40)만 남기고 아래로 숨김 */
className={`
fixed left-0 right-0 bottom-0 z-30
mx-auto w-full
bg-white
transition-transform duration-300 ease-out
h-[320px]
${scriptBoxDock.value ? 'translate-y-[calc(100%-40px)]' : 'translate-y-0'}
`}
>
{/* 변경: 상단바 내부 레이아웃(좌/우 정렬) */}
<div className="h-10 px-5 py-2 flex items-center justify-between border-b border-zinc-200">
{/* 슬라이드 제목 변경 */}
<SlideTitle
slideNameChange={slideNameChange}
slideTitle={slideTitle}
setSlideTitle={setSlideTitle}
/>

{/* 우측 컨트롤 영역 */}
<div className="flex items-center gap-4">
{/* 이모지 카운트 영역 -> 컴포넌트로 분리 */}
<ScriptBoxEmogi isEmogiClick={isEmogiClick} handleEmogiClick={handleEmogiClick} />

{/* 변경기록/의견 버튼 그룹 */}
<div className="flex items-center gap-2">
{/* 변경기록 */}
<ScriptHistory scriptHistory={scriptHistory} />

{/* 의견(댓글)기록 */}
<Opinion
opinion={opinion}
activeReplyIdx={activeReplyIdx}
setActiveReplyIdx={setActiveReplyIdx}
replyText={replyText}
setReplyText={setReplyText}
/>
</div>

{/* 전체 Script Box 열림 닫힘 버튼*/}
<button
type="button"
onClick={scriptBoxDock.toggle}
className="w-6 h-6 rounded hover:bg-zinc-100 flex items-center justify-center"
aria-label="more"
aria-expanded={!scriptBoxDock.value}
>
<img
src={smallArrowIcon}
className={`
w-4 h-4
transition-transform duration-300 ease-out
${scriptBoxDock.value ? 'rotate-180' : 'rotate-0'}
`}
/>
</button>
</div>
</div>

{/* "대본 입력 영역" textarea */}
<div className="w-full bg-white">
<textarea
className="
w-full min-h-[250px] resize-none
px-5 py-4
text-zinc-700 text-sm leading-6
placeholder:text-gray-400
outline-none
rounded-bl-lg rounded-br-lg
"
placeholder="슬라이드 대본을 입력하세요..."
/>
</div>
</div>
</>
);
};

export default ScriptBox;
Loading