Skip to content
Open
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
80 changes: 73 additions & 7 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* {
margin: 0;
padding: 0;
height : 100%
box-sizing: border-box;
font-family: "Pretendard", sans-serif; /*프로젝트 전체 폰트 적용*/
}
Expand Down Expand Up @@ -51,7 +52,7 @@ html, body {
font-family: "Pretendard", sans-serif;
text-align: left;
margin: 0 0 24px 0;
line-height: 36px; /* 150% */
line-height: 36px;
}

/* 리스트 간격 */
Expand All @@ -66,7 +67,7 @@ html, body {
background-color: #ffffff;
padding: 16px;
border-radius: 12px; /* border-radius 값 */
border: 1px solid #e0e0e0; /* 테두리 추가 */
border: 1px solid #fff;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.10);/* Shadow 값 */
transition: transform 0.2s ease;

Expand All @@ -85,18 +86,17 @@ html, body {
}


/* 체크된 텍스트의 스타일 (취소선과 흐린 색상) */
.done-text {
text-decoration: line-through;
color: #9CA3AF; /* 시안과 비슷한 흐린 회색 */
color: #9CA3AF;
}

/* 체크박스와 텍스트를 가로로 예쁘게 배치하기 위해 추가 (필요시) */
/* 체크박스와 텍스트를 가로로 배치 */
.todo-card {
display: flex;
align-items: center; /* 세로 중앙 정렬 */
gap: 12px; /* 체크박스와 글자 사이 간격 */
/* 기존 테두리, 배경색 등 스타일 유지 */
gap: 12px;

}


Expand Down Expand Up @@ -131,3 +131,69 @@ html, body {

}



/* 휴지통 버튼 */
.delete-btn {
color: #0A0A0A;
margin-left: auto;
background: none;
border: none;
font-size: 20px;
font-style: normal;
font-weight: 500;
cursor: pointer;
}


/* 입력창 컨테이너 */
.todo-input-container {
display: flex;
gap: 12px;
margin-bottom: 24px;
height: 46px;
}

/* 입력 필드 */
.todo-input {
flex: 1;
height: 100%;
padding: 0 16px;
border-radius: 8px;
border: 1px solid #E5E7EB;
font-size: 14px;
color: #1F2937;
background-color: #ffffff;
box-sizing: border-box;
transition: border-color 0.2s, box-shadow 0.2s;
}

/*할 일을 입력하세요*/
.todo-input::placeholder {
color: #6B7280;
font-weight: 400;
}

/* 포커스(입력 중) 상태일 때 파란색 테두리 효과 */
.todo-input:focus,
.todo-input.focused-input {
border: 2px solid #3B82F6;
box-shadow: 0 0 0 4px #3B82F6;
background: rgba(255, 255, 255, 0.00);


}

/* 추가 버튼 */
.todo-submit-btn {
height: 100%;
padding: 0 28px;
background-color: #3B82F6;
color: #ffffff;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
110 changes: 95 additions & 15 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,119 @@

import { useState } from 'react';
import TodoHeader from './components/TodoHeader';
import TodoList from './components/TodoList';
import './App.css';

const App = () => {
// 1. 위쪽에 보여줄 데이터 (꽉 찬 배열)
const populatedTodos = [
{ id: 1, content: "리액트 공식문서 읽기", isDone: true },
{ id: 2, content: "알고리즘 문제 풀기", isDone: true },
{ id: 3, content: "운동 30분 하기", isDone: false },
{ id: 4, content: "프로젝트 회의 준비", isDone: false },
];
// ==========================================
// 1. 위쪽 화면 (기본 상태)
// ==========================================
const [topInputText, setTopInputText] = useState("");
const [topTodos, setTopTodos] = useState([
{ id: 1, text: "리액트 공식문서 읽기", completed: true },
{ id: 2, text: "알고리즘 문제 풀기", completed: true },
{ id: 3, text: "운동 30분 하기", completed: false },
{ id: 4, text: "프로젝트 회의 준비", completed: false },
{ id: 5, text: "장보기 하기", completed: false },
]);

// 상단 추가 함수
const handleAddTop = () => {
if (topInputText.trim() === "") return; // 빈 문자열 방지
const newTodo = {
id: Date.now(), // 고유 ID 생성
text: topInputText,
completed: false,
};
setTopTodos([...topTodos, newTodo]);
setTopInputText(""); // 입력창 초기화
};

// 상단 삭제 함수
const handleDeleteTop = (id: number) => {
setTopTodos(topTodos.filter((todo) => todo.id !== id));
};


// ==========================================
// 2. 아래쪽 화면 (Focus 상태)
// ==========================================
const [bottomInputText, setBottomInputText] = useState("");
const [bottomTodos, setBottomTodos] = useState([
{ id: 3, text: "운동 30분 하기", completed: false },
{ id: 4, text: "프로젝트 회의 준비", completed: false },
]);

// 하단 추가 함수
const handleAddBottom = () => {
if (bottomInputText.trim() === "") return;
const newTodo = {
id: Date.now(),
text: bottomInputText,
completed: false,
};
setBottomTodos([...bottomTodos, newTodo]);
setBottomInputText("");
};

// 하단 삭제 함수
const handleDeleteBottom = (id: number) => {
setBottomTodos(bottomTodos.filter((todo) => todo.id !== id));
};

// 2. 아래쪽에 보여줄 데이터 (텅 빈 배열)
const emptyTodos: any[] = [];

return (
<div className="app-layout">
<div className="todo-container">

{/* ========================================= */}
{/* 첫 번째 화면: 데이터가 있을 때 (체크박스 토글) */}
{/* 첫 번째 화면: 기본 상태 */}
{/* ========================================= */}
<div className="section">
<TodoHeader />
<TodoList todos={populatedTodos} />
<div className="todo-input-container">
<input
type="text"
className="todo-input"
placeholder="할 일을 입력하세요"
value={topInputText}
onChange={(e) => setTopInputText(e.target.value)}
/>
{/* 추가 버튼 클릭 시 handleAddTop 실행 */}
<button className="todo-submit-btn" onClick={handleAddTop}>
추가
</button>
</div>

{/* 삭제 함수를 TodoList로 전달 */}
<TodoList todos={topTodos} onDelete={handleDeleteTop} />
</div>


{/* ========================================= */}
{/* 두 번째 화면: 데이터가 없을 때 (빈 상태) */}
{/* 두 번째 화면: 입력 중 (Focus) 상태 */}
{/* ========================================= */}
<div className="section" style={{ marginTop: '96px' }}>

<p style={{ fontSize: '14px', color: '#6B7280', margin: '0 0 16px 0' }}>
Week 3 — 입력 중 (Focus)
</p>
<TodoHeader />
<TodoList todos={emptyTodos} />

<div className="todo-input-container">
<input
type="text"
className="todo-input focused-input"
placeholder="새로운 할 일"
value={bottomInputText}
onChange={(e) => setBottomInputText(e.target.value)}
/>
{/* 추가 버튼 클릭 시 handleAddBottom 실행 */}
<button className="todo-submit-btn" onClick={handleAddBottom}>
추가
</button>
</div>

{/* 삭제 함수를 TodoList로 전달 */}
<TodoList todos={bottomTodos} onDelete={handleDeleteBottom} />
</div>

</div>
Expand Down
51 changes: 29 additions & 22 deletions src/components/TodoCard.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
import React from 'react';

// 부모로부터 onDelete 함수를 받을 수 있도록 타입 추가
interface TodoCardProps {
content: string;
isDone: boolean;
text: string;
completed: boolean;
onDelete: () => void;
}

const TodoCard = ({ content, isDone }: TodoCardProps) => {
const TodoCard = ({ text, completed, onDelete }: TodoCardProps) => {
return (
<div className={`todo-card ${isDone ? 'done-card' : ''}`}>
<div className="checkbox-icon">
{isDone ? (
// 🔵 완료 상태 (파란색 채워진 원 + 흰색 체크)
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#A4C5FD"/>
<path d="M7 12.5L10 15.5L17 8.5" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
) : (
// ⚪ 미완료 상태 (회색 테두리 빈 원)
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="#E5E7EB" strokeWidth="2"/>
</svg>
)}
<div className={`todo-card ${completed ? 'done-card' : ''}`}>

<div className="todo-info">
<div className="checkbox-icon">
{completed ? (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#A4C5FD"/>
<path d="M7 12.5L10 15.5L17 8.5" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
) : (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="#E5E7EB" strokeWidth="2"/>
</svg>
)}
</div>

<p className={`card-text ${completed ? 'done-text' : ''}`}>
{text}
</p>
</div>

<p className={`card-text ${isDone ? 'done-text' : ''}`}>
{content}
</p>
{/* onClick에 넘겨받은 onDelete 연결 */}
<button className="delete-btn" onClick={onDelete}>
🗑
</button>

</div>
);
};
Expand Down
23 changes: 11 additions & 12 deletions src/components/TodoList.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
// src/components/TodoList.tsx
import React from 'react';
import TodoCard from './TodoCard';

// 1. 부모에게서 받을 데이터의 타입을 정해줍니다.
interface Todo {
id: number;
content: string;
isDone: boolean;
text: string;
completed: boolean;
}

// 부모로부터 onDelete 함수를 받을 수 있도록 타입 추가
interface TodoListProps {
todos: Todo[];
todos: Todo[];
onDelete: (id: number) => void;
}

// 2. 괄호 안에 { todos }: TodoListProps 를 넣어서 외부 데이터를 받아옵니다!
// 🚨 주의: 이 아래에 const todos = [...] 같은 코드가 절대 있으면 안 됩니다!
const TodoList = ({ todos }: TodoListProps) => {
const TodoList = ({ todos, onDelete }: TodoListProps) => {
return (
<div className="todo-list-wrapper">

{/* 3. 받아온 todos 배열의 길이에 따라 빈 화면을 보여줄지 결정합니다 */}
{todos.length === 0 ? (
<div className="empty-state">
<span className="empty-icon">📋</span>
Expand All @@ -30,8 +26,11 @@ const TodoList = ({ todos }: TodoListProps) => {
{todos.map((todo) => (
<TodoCard
key={todo.id}
content={todo.content}
isDone={todo.isDone}
text={todo.text}
completed={todo.completed}

// 삭제 버튼 클릭 시 현재 항목의 id를 담아 실행하도록 함수 전달
onDelete={() => onDelete(todo.id)}
/>
))}
</div>
Expand Down