로보테크AI

융합_로보테크 AI 자율주행 로봇 개발자 과정-26/05/14[프롬프트 + 웹]

steezer 2026. 5. 14. 18:30

TCREI

작업Task, 콘텍스트Context, 참고자료Reference, 평가Evaluate, 반복Iterate의 약자

- 작업: 작업을 명확하게 정의하기
- 콘텍스트(맥락): 충분한 콘텍스트와 배경 지식 제공하기
- 참고자료: 예시와 참고자료 활용하기
- 평가: 출력 평가하기
- 반복: 반복적으로 개선하기

 

Task : 작업 명확히 정의하기

- 행위 동사: 무엇을 해야 하는가?
- 대상: 무엇에 대해 작업하는가?
- 조건: 작업 수행을 위해 지켜야 할 것은 무엇인가?
- 기대 결과: 작업 완료 시 어떤 상태가 되어야 하는가?

출력 형식 명시하기, 나눠서 지시하기

 

Context : 충분한 배경 지식 제공하기

효과적인 콘텍스트 제공하기
콘텍스트를 주입하는 것이 특히 효과적인 상황
- 기존 코드베이스와 통합해야 할 때: 기존 코드의 패턴, 네이밍 컨벤션, 아키텍처를 알아야 일관된 코드를 생성
- 특정 라이브러리, 프레임워크를 사용할 때: 버전별로 API가 다르므로 버전 정보는 필수
- 비즈니스 도메인 특화 작업일 때: 도메인 용어와 비즈니스 규칙 설명 필요
- 컨벤션 준수가 필요할 때: 코딩 스타일, 커밋 메시지 규칙 등을 알려주면 준수한 코드 생성

 

Reference : 예시와 참조 활용하기

Few-Shot 프롬프팅

AI에게 몇 가지 예시를 제공하는 방법

효과적인 예시 제공하기
예를 선택할 때 대표성, 완전성, 복잡도, 품질을 고려
- 대표성: 생성하려는 코드와 유사한 구조의 예시 선택
- 완전성: 예시는 완전해야 합니다. 일부만 보여주면 AI가 나머지를 추측함
- 복잡도: 단순한 코드를 만들 때 복잡한 예시 주지 말것 예시가 너무 복잡하면 핵심 패턴이 묻힘
- 품질: 예시 코드 자체가 좋은 코드여야 함. AI는 예시의 나쁜 패턴도 따라함
이 내용을 기반으로 아키텍처나 코딩 패턴 등을 미리 정의해서 입력해두는 것도 효율적인 방법

 

Evaluate & Iterate : 평가하고 반복하기

평가 기준 설정
좋은 프롬프트와 나쁜 프롬프트를 가르는 기준이 필요

내가 만든 프롬프트가 좋은 프롬프트인지 알고 싶다면 프롬프트로 생성한 대답을 평가해야 함

특히 코드를 생성했다면 품질 평가가 중요

AI가 생성한 코드를 평가할 때 다음 기준을 적용

평가도 AI에게 하도록 지시하고 요약만 요청할 수 있음
- 기능적 정확성: 요구사항을 충족하는지, 버그가 없는지
- 코드 품질: 가독성이 좋은지, 스타일이 일관적인지
- 성능: 불필요한 연산이 없는지, 메모리를 효율적으로 사용하는지
- 보안: 입력 검증이 적절한지, 민감 정보가 노출되지 않는지
- 유지보수성: 테스트하기 쉬운 구조인지, 문서화가 적절한지

 


웹 서비스를 만드는 기술

 

인터넷

여러 대 컴퓨터를 하나로 연결해주는 거대한 통신망

 

인터넷 상의 컴퓨터들 간에 정보를 공유할 수 있도록 해주는 네트워크 시스템

 

웹 개발

인터넷과 웹을 통해 공유할 웹 사이트를 만드는 일과 이를 서비스하기 위해 필요한 다양한 환경을 구축하는 일

ㄴHTML, CSS

 

HTML

HyperText Markup Language

하이퍼 텍스트와 콘텐츠를 표시해주는 언어

HTML5이 현재 표준

기본 문법 tag(태그명, 시작 태그, 끝 태그, 콘텐츠)

속성(태그의 부가적 가능 정의, 선택사항)

ㄴ<태그명 속성="값">콘텐츠</태그명>

ㄴ<태크명 속성="값" 속성="값"/>

 

주석

코드 메모

<!-- 내용 -->

 

HTML 문서 구조

<!DOCTYPE html>

<html>

 <head>

  문서 정보

 </head>

 <body>

  화면에 표시될 내용

 </body>

</html>

 

 

실습

더보기

HTML은 다양한 종류의 태그를 지원합니다.

‘가장 많이 사용되는 태그 Top 20’을 조사하고, 태그 이름과 용도를 정리해 보세요.

순위 태그 카테고리 용도
1 <html> 문서 구조 HTML 문서의 최상위 루트 요소
2 <head> 문서 구조 메타데이터(제목·스타일·스크립트 링크) 컨테이너
3 <body> 문서 구조 화면에 실제로 표시되는 콘텐츠 영역
4 <meta> 메타데이터 문자 인코딩·뷰포트·SEO 설명 등 페이지 정보 선언
5 <title> 메타데이터 브라우저 탭/검색 결과에 표시되는 페이지 제목
6 <div> 레이아웃 의미 없는 범용 블록 컨테이너 (CSS 그룹화용)
7 <link> 외부 리소스 외부 CSS·파비콘 등을 문서에 연결
8 <a> 링크 하이퍼링크 (href로 다른 페이지·앵커 이동)
9 <script> 스크립트 JavaScript 코드 삽입 또는 외부 JS 파일 연결
10 <img> 미디어 이미지 표시 (src, alt 필수)
11 <span> 레이아웃 의미 없는 인라인 컨테이너 (텍스트 일부 스타일링용)
12 <p> 텍스트 문단 (paragraph)
13 <li> 리스트 리스트 항목 (<ul> 또는 <ol> 내부)
14 <ul> 리스트 순서 없는 리스트 (불릿)
15 <style> 스타일 문서 내부에 CSS 직접 작성
16 <input> 사용자 입력 필드 (text, checkbox, file 등 type으로 결정)
17 <br> 텍스트 강제 줄바꿈 (빈 요소)
18 <form> 사용자 입력을 서버로 전송하는 폼 컨테이너
19 <h1>~<h6> 텍스트 제목 (heading), 숫자가 작을수록 상위 제목
20 <button> 폼/UI 클릭 가능한 버튼 (폼 제출·이벤트 트리거)

 

더보기
  1. 조사한 내용을 바탕으로, 다음과 같은 형태의 페이지를 만들어 보세요. 인물은 자유롭게 변경 가능.
  2. 다음 페이지를 만든 프롬프트를 정리해서 메모장에 저장해두세요! 제가 확인할 겁니다.

ex)

프롬프트 입력-claude 기준(계정 프롬프트 및 스킬, 커넥터는 표시 X)

더보기

스티브 잡스 소개용 단일 HTML 파일 만들기

외부 CSS/JS 없이 <style> 태그 하나로 처리하고, 인물 사진은 이미지 데이터를 직접 삽입할 예정

(이미지 주소: https://cdn.maily.so/p06mjj9yg8dbnhxeljo7lsbj64t4)


[레이아웃 - 위에서 아래 순서]

1. 페이지 제목 (h1): "스티브 잡스"
2. 부제: "애플의 창업자, 혁신의 아이콘!"
3. 인물 사진 (폭, 비율 유지)
4. 사진 캡션 한 줄: "무대 위에서 발표하던 그의 모습이 담긴 사진. 검은 터틀넥과 청바지가 그의 상징이었다."
5. "스티브 잡스의 생애:"
6. 연도별 불릿 리스트 (10~13개, "YYYY - 사건" 형식)
7. 대표 명언 (굵은 큰따옴표): "늘 갈망하라, 늘 우직하라."
8. 작성자 표기: "- 스티브 잡스 -"
9. 마지막 줄: "스티브 잡스에 대한 더 자세한 이야기는 그의 [위키피디아 페이지에서]." (대괄호 부분만 하이퍼링크)


[스타일]

* 흰 배경, 검정 글자
* 링크는 파란색 + 밑줄
* 리스트 들여쓰기
* 작성자 표기에 글자 기울기 적용
* 1, 2, 5, 6(연도에만), 7, 8, 9(글씨 굵게)
* 볼드체 strong, 이미지 img, 목록 ul + li, 인용문 q, 이탤릭체 em, 하이퍼텍스트 a 적용


[연도 - 다음 항목 반드시 포함]

 

1955 출생

1976 애플 창업

1984 Macintosh

1985 애플 해임

1986 픽사 인수

1997 애플 복귀

2001 iPod

2007 iPhone

2010 iPad

2011 사망


[제약]

* HTML 코드 하나만 (CSS/JS 분리 금지)
* 외부 폰트 로드 금지
* 코드와 함께 예상 출력 결과 제공할 것

 

claude가 작성한 html

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>스티브 잡스</title>
<style>
  body {
    background-color: #ffffff;
    color: #000000;
    font-family: sans-serif;
    max-width: 720px;
    margin: 40px auto;
    padding: 0 20px;
    line-height: 1.6;
  }
  h1 {
    font-weight: bold;
  }
  .subtitle {
    font-weight: bold;
    font-size: 1.1em;
  }
  img {
    max-width: 100%;
    height: auto;
    display: block;
    margin: 16px 0 8px 0;
  }
  .caption {
    font-size: 0.95em;
    margin-bottom: 24px;
  }
  .life-heading {
    font-weight: bold;
  }
  ul {
    padding-left: 40px;
  }
  ul li .year {
    font-weight: bold;
  }
  .quote {
    display: block;
    font-size: 1.4em;
    font-weight: bold;
    margin: 24px 0 8px 0;
  }
  .author {
    font-style: italic;
    font-weight: bold;
    margin-bottom: 24px;
  }
  .more {
    font-weight: bold;
  }
  a {
    color: #0000ee;
    text-decoration: underline;
  }
</style>
</head>
<body>
  <h1>스티브 잡스</h1>
  <p class="subtitle">애플의 창업자, 혁신의 아이콘!</p>
  <img src="이미지 링크" alt="스티브 잡스">
  <p class="caption">무대 위에서 발표하던 그의 모습이 담긴 사진. 검은 터틀넥과 청바지가 그의 상징이었다.</p>
  <p class="life-heading">스티브 잡스의 생애:</p>
  <ul>
    <li><span class="year">1955</span> - 샌프란시스코에서 출생</li>
    <li><span class="year">1976</span> - 스티브 워즈니악과 함께 애플 컴퓨터 창업</li>
    <li><span class="year">1984</span> - Macintosh 출시, 개인용 컴퓨터의 GUI 시대 개막</li>
    <li><span class="year">1985</span> - 경영 갈등으로 애플에서 해임</li>
    <li><span class="year">1986</span> - 픽사(Pixar) 인수</li>
    <li><span class="year">1995</span> - 픽사의 토이 스토리 개봉, 흥행 성공</li>
    <li><span class="year">1997</span> - 애플에 복귀, 임시 CEO 취임</li>
    <li><span class="year">2001</span> - iPod 발표, 음악 산업의 디지털 전환 주도</li>
    <li><span class="year">2007</span> - iPhone 발표, 스마트폰 시대의 시작</li>
    <li><span class="year">2010</span> - iPad 발표, 태블릿 시장 개척</li>
    <li><span class="year">2011</span> - 애플 CEO 사임, 같은 해 췌장암으로 사망</li>
  </ul>
  <q class="quote">늘 갈망하라, 늘 우직하라.</q>
  <p class="author"><em>- 스티브 잡스 -</em></p>
  <p class="more">스티브 잡스에 대한 더 자세한 이야기는 그의 <a href="https://ko.wikipedia.org/wiki/%EC%8A%A4%ED%8B%B0%EB%B8%8C_%EC%9E%A1%EC%8A%A4">위키피디아 페이지에서</a>.</p>
</body>
</html>

 


CSS

Cascading Stylesheet
연속적으로 스타일을 정의하는 문서

태그와 태그가 중첩되어 연속적인 구조를 지닌 HTML 문서에 스타일 추가

CSS의 역할은 HTML 문서에 스타일을 추가하는 것

 

스타일 선언문 형태

선택자{

    속성명: 속성값;

}

선택자: 어떤 요소에 스타일을 적용할지에 대한 정보

{}: 선택한 요소에 적용할 스타일을 정의하는 영역

속성명: 어떤 스타일을 정의하고 싶은지에 대한 정보

속성값: 어떻게 정의하고 싶은지에 대한 정보

CSS 주석

/* 주석 내용 */

CSS3가 현재 표준

 

HTML 문서에 CSS문서를 적용할 때 사용 방식

인라인 스타일: 태그에 직접 기술

스타일 태그: 스타일시트를 위한 태그를 추가하여 기술하기

문서 간의 연결: 스타일시트 문서를 따로 작성하여 HTML 문서와 연결하기

 

인라인 스타일

태그에 style 속성을 추가하여 요소에 직접적으로 스타일 정의하는 방식

<p style="color: blue;">

    글자를 파란색으로
</p>

 

스타일 태그

HTML 문서에 <style></style> 태그를 추가하여 그 안에 CSS 코드 작성 가능

내부 스타일시트라 표현하기도 함

<style>

    /* style 태그 안에는 CSS 코드를 작성해야 함*/

    p{ color: red; }

</style>

 


## 실습 과제 : CSS 속성 조사 및 페이지 만들기

1. CSS는 다양한 종류의 스타일 속성 지원합니다. ‘가장 많이 사용되는 태그 Top 30’을 조사하고, 속성 이름에 따른 값 유형을 정리해 보세요. 
2. 조사한 내용을 바탕으로, 다음 과 같은 형태의 웹 서비스를 만들어보세요.
3. 완성하는데 작성한 프롬프트를 기록해두세요 (TCREI 프레임워크 권장)
    
    ⇒ https://coconuttalk-sample.netlify.app/

 

코코넛톡 - 로그인

코코넛톡 코코넛 계정 찾기   |   비밀번호 재설정

coconuttalk-sample.netlify.app

 

1. 레이아웃 / 박스 모델

display 키워드 block, inline, flex, grid, none
position 키워드 static, relative, absolute, fixed, sticky
width <length> | <percentage> | auto 200px, 50%, 100vw, auto
height <length> | <percentage> | auto 300px, 100vh, auto
margin <length> | <percentage> | auto (1~4개) 10px, 0 auto, 10px 20px
padding <length> | <percentage> (1~4개) 8px, 10px 20px
box-sizing 키워드 content-box, border-box
overflow 키워드 visible, hidden, scroll, auto

2. 색상 / 배경

color <color> #333, rgb(0,0,0), red, hsl(0,100%,50%)
background-color <color> #fff, transparent, rgba(0,0,0,0.5)
background-image <image> (url(), <gradient>) url("bg.png"), linear-gradient(...)
opacity <number> (0.0 ~ 1.0) 0, 0.5, 1

3. 테두리

border <line-width> <line-style> <color> (단축) 1px solid #000
border-radius <length> | <percentage> 4px, 50%, 8px 16px
box-shadow <offset-x> <offset-y> <blur> <spread> <color> 0 2px 4px rgba(0,0,0,0.1)

4. 텍스트 / 폰트

font-family <family-name> 콤마 리스트 "Pretendard", sans-serif
font-size <length> | <percentage> | 키워드 16px, 1.2rem, large
font-weight <number> (100~900) | 키워드 400, 700, bold, normal
text-align 키워드 left, center, right, justify
line-height <number> | <length> | <percentage> 1.5, 24px, 150%
text-decoration 키워드 조합 none, underline, line-through

5. Flexbox / Grid

flex-direction 키워드 row, column, row-reverse
justify-content 키워드 flex-start, center, space-between
align-items 키워드 stretch, center, flex-end
gap <length> | <percentage> (1~2개) 8px, 10px 20px

6. 인터랙션 / 변형

cursor 키워드 | <url> pointer, default, not-allowed
transition <property> <duration> <timing> <delay> all 0.3s ease, opacity 200ms
transform <transform-function> 리스트 translateX(10px), rotate(45deg) scale(1.2)
z-index <integer> | auto 0, 10, -1, auto

TCREI는 Claude 계정 프롬프트 내에 입력 되어 있음 + 로보테크 프로젝트 자체에 넣은 프롬프트는 아래 설명에 제외

아래는 web을 만들 때 입력한 프롬프트

더보기

확정된 구조 결정:
- common.css는 components가 아닌 pages 폴더에 둠
- chats.html은 전용 CSS 없음. friends.css와 userlist.css를 재사용
- 채팅 미리보기(.user__message)와 시간(.user__time)은 userlist.css에 정의해서
  친구 카드/채팅 카드 둘 다 커버

## 2. 디자인 시스템 (common.css)

CSS 변수(:root)로 컬러 팔레트 정의:
- --primary: #4A90E2 (메탈릭 블루)
- --primary-dark: #357ABD
- --secondary: #2C3E50 (다크 네이비)
- --bg: #F5F7FA (페이지 배경)
- --surface: #FFFFFF (카드/컨테이너 배경)
- --text: #1A202C (본문 텍스트)
- --text-sub: #718096 (보조 텍스트)
- --border: #E2E8F0 (구분선)

기본 설정:
- 폰트: "Malgun Gothic", "맑은 고딕", sans-serif
- 본문 글자 크기: 14px
- 모든 요소에 margin/padding 0, box-sizing border-box
- a 태그: text-decoration none, color inherit
- ul: list-style none
- input/textarea/button: outline none, border none, font-family inherit

.container 사양:
- 폭 400px, 높이 700px
- margin: 30px auto (화면 중앙 배치)
- 흰 배경, 둥근 모서리 12px
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08)
- overflow hidden, position relative

## 3. 페이지별 사양

### index.html (로그인)

위에서 아래 순서:
1. CSS로 그린 로봇 로고 (80×90px)
2. 제목 "로보클럽" (h3, 28px, 굵게, 다크 네이비, letter-spacing -0.5px)
3. 폼 (action="./friends.html")
   - 아이디 입력 (placeholder "로보클럽 계정 (이메일 또는 전화번호)")
   - 비밀번호 입력 (type password, placeholder "비밀번호")
   - 로그인 버튼 (value "로그인")
4. 하단 텍스트 "계정 찾기 | 비밀번호 재설정" (12px, 회색)

로봇 로고 구성 (CSS 의사 요소로):
- 안테나: ::before로 가로 4px 세로 14px 메탈릭 블루 막대, 상단 정중앙
- 안테나 끝점: ::after로 10px 메탈릭 블루 동그라미, 안테나 위
- 머리: .login__head, 70×60px, 메탈릭 블루 배경, border-radius 14px
- 눈 2개: .login__eye, 12px 흰 동그라미, flex gap 10px로 가로 배치

입력 필드:
- 높이 44px, 좌우 패딩 14px
- 배경 var(--bg), 테두리 1px var(--border), border-radius 8px
- :focus 시 테두리 메탈릭 블루

로그인 버튼:
- 높이 46px, 메탈릭 블루 배경, 흰 글씨, 굵게
- border-radius 8px, cursor pointer
- :hover 시 --primary-dark

레이아웃: .login은 height 100%, flex column, center 정렬, 좌우 패딩 40px

### friends.html (친구 목록)

위에서 아래 순서:
1. 상단 헤더 (.header)
   - 좌측 제목 "친구" (h3, 18px, 굵게)
   - 우측 아이콘: 검색 🔍, 추가 + (gap 14px, 회색)
2. 내 프로필 카드 (.user.my-profile)
   - 큰 아바타 52px + "수강생0 (나)" + "로보틱스 부트캠프 진행 중"
   - 하단 테두리 1px
3. 구분선 "수강생 10" (.friends__menu, 12px, 굵게, 회색)
4. 친구 카드 10개 (수강생1~수강생10)
5. 하단 네비게이션 (사람 👤 active, 말풍선 💬)

친구 상태 메시지 (순서대로):
1. "ROS2 공부 중입니다"
2. "YOLO 모델 학습 돌리는 중"
3. "SLAM 디버깅 도와주세요"
4. "아두이노 납땜 마스터"
5. "PyQt5 GUI 개발 중"
6. "Nav2 자율주행 테스트"
7. "ESP-01 펌웨어 굽는 중"
8. "TurtleBot3 캘리브레이션"
9. "CNN 학습 중 - 정확도 0.92"
10. "팀 프로젝트 발표 준비"

friends.css 구성:
- .friends: height calc(100% - 60px), overflow-y auto
- .friends__menu: padding 14px 20px 6px
- .user-list: padding-bottom 70px (네비게이션 가림 방지)

### chats.html (채팅 목록)

위에서 아래 순서:
1. 상단 헤더 ("채팅" + 검색/추가 이모지)
2. 채팅방 카드 4개 (각각 a 태그로 감싸서 chat.html로 이동)
3. 하단 네비게이션 (말풍선 active)

채팅방 4개 데이터 (닉네임 / 메시지 미리보기 / 시간):
1. "수강생3" / "SLAM 노드가 자꾸 죽는데 혹시 봐줄 수 있어?" / "오후 2:34"
2. "팀 프로젝트방" / "내일 발표자료 최종본 공유합니다" / "오후 1:12"
3. "수강생5" / "PyQt 위젯 정렬 어떻게 했어?" / "오전 11:45"
4. "강사님" / "금요일 코드 리뷰 일정 안내드립니다" / "오전 9:20"

전용 CSS 없음 (확정): friends.css와 userlist.css만 link.
채팅 카드는 친구 카드와 동일한 .user 클래스 사용, 차이점은
- .user__state 대신 .user__message 사용
- .user__time 추가 (우측 상단 absolute)

### chat.html (1:1 채팅방)

위에서 아래 순서:
1. 상단 바 (.chat-screen__bar, 70px, 하단 테두리)
   - 상대 아바타 40px + "수강생3" + "👤 2" (참여자 수)
2. 메시지 목록 (.chat-screen__comments, 스크롤, 배경 --bg)
   - 패딩 16px 20px, flex column, gap 8px
3. 하단 입력 영역 (.chat-form, 70px, 상단 테두리)
   - 좌측 플러그인 이모지 3개: 😊 📎 📅 (gap 10px)
   - 텍스트 입력창 (textarea, 40px 높이, 둥근 모서리 20px)
   - 전송 버튼 (메탈릭 블루, 둥근 모서리 20px, "전송")
4. 하단 네비게이션 없음 (전체 화면 채팅)

메시지 말풍선 사양:
- 최대 폭 75%, 패딩 10px 14px, 글자 13px, line-height 1.5
- 둥근 모서리 14px, word-break break-word
- 상대 메시지(.comment): 흰 배경, 1px 테두리, 좌측 정렬 (align-self flex-start)
- 내 메시지(.comment.mine): 메탈릭 블루 배경, 흰 글씨, 우측 정렬

메시지 11개 (순서대로):
1. 상대: "SLAM 노드가 자꾸 죽어"
2. 상대: "에러 로그는 봤어?"
3. 내: "방금 봤는데 transform이 안 잡힌대"
4. 내: "odom → base_link 연결이 끊긴 것 같아"
5. 상대: "tf_static 발행은 되고 있어?"
6. 내: "잠깐만 ros2 topic echo로 확인해볼게"
7. 내: "아 발행이 안 되고 있네"
8. 상대: "그럼 launch 파일에서 static_transform_publisher 빠진 거 아니야?"
9. 내: "맞다 그거였어"
10. 내: "고마워 살았다"
11. 상대: "ㅋㅋ 다음에 커피 사"

## 4. 공통 컴포넌트 사양

### navbar.css (하단 네비게이션)

- position absolute, bottom 0, 폭 100%, 높이 60px
- 흰 배경, 상단 1px 테두리, z-index 10
- ul: 폭 100%, flex 가로 배치, 각 li flex 1로 균등 분할
- a: 비활성 시 회색, .active 클래스 시 메탈릭 블루
- 사용 이모지: 사람 👤, 말풍선 💬

### header.css (상단 헤더)

- 높이 56px, 좌우 패딩 20px
- flex로 좌(제목) 우(메뉴) 양쪽 정렬, 하단 1px 테두리
- .header__title: 18px, 굵게, 다크 네이비
- .header__menu: flex, gap 14px, 회색, cursor pointer
- 메뉴 hover 시 메탈릭 블루

### userlist.css (유저 카드 + 로봇 아바타)

이 파일이 가장 복잡함. 친구 카드/채팅 카드/내 프로필 모두 커버.

- .user: flex 가로 배치, 패딩 12px 20px, position relative, cursor pointer
- .user:hover: 배경 var(--bg)
- .user__column:first-child: margin-right 12px

CSS로 그린 로봇 아바타 (.user__pic):
- 44×44px, 메탈릭 블루 배경, border-radius 12px, position relative
- ::before로 안테나 (3×6px 흰 막대, top 6px, 가로 중앙)
- ::after로 머리/얼굴 (18×14px 흰 사각형, top 18px, 가로 중앙, border-radius 4px)
- ::after에 box-shadow로 양쪽 눈 표현:
  box-shadow: -5px 4px 0 -3px var(--primary), 5px 4px 0 -3px var(--primary)

텍스트:
- .user__nick: 14px, 굵게, margin-bottom 2px
- .user__state, .user__message: 12px, 회색
- .user__time: position absolute, top 14px, right 20px, 11px, 회색

내 프로필 (.my-profile):
- 패딩 20px, 하단 1px 테두리
- 아바타 더 크게: 52px

채팅 화면용 (.chat-user):
- 패딩 12px 20px
- 아바타 40px
- .user__message는 flex로 이모지+숫자 표시

## 5. 페이지 간 연결

- index.html 로그인 폼 → friends.html
- friends.html 하단 네비 💬 → chats.html
- chats.html 채팅방 카드 클릭 → chat.html
- chats.html 하단 네비 👤 → friends.html
- chat.html에는 뒤로 가기 없음 (이번 범위 밖)


[Evaluate - 성공 기준]

- 모든 파일이 사양서대로 생성됨 (11개)
- index.html을 브라우저로 열면 로봇 로고가 보이고 로그인 화면이 정상 렌더링
- 로그인 버튼 클릭 시 friends.html로 이동
- 친구 페이지에서 수강생 10명 + 상태 메시지가 사양서 순서대로 표시
- 하단 네비게이션으로 친구/채팅 페이지 전환 가능
- 채팅 목록에서 카드 클릭 시 chat.html로 이동
- 채팅방에서 메시지 11개가 사양서 순서대로 표시되며 내/상대 말풍선 색이 구분됨
- CSS만으로 그린 로봇 아바타가 모든 페이지에서 동일하게 보임
- F12 Console에 404나 에러 없음

테스트 방법:
1. roboclub/ 폴더 위치에서 `python -m http.server 8000` 실행
2. 브라우저에서 http://localhost:8000 접속
3. 4개 페이지 모두 순회하며 위 항목 체크


[Iterate - 진행 방식]

단계 분할로 진행. 한 번에 모든 파일을 만들면 검토하기 어려움.

진행 순서 (확정, 변경 금지):
1단계: css/pages/common.css (디자인 시스템 기반)
2단계: index.html + css/pages/index.css (로그인 페이지)
3단계: css/components/navbar.css + header.css + userlist.css (공통 컴포넌트)
4단계: friends.html + css/pages/friends.css (친구 목록)
5단계: chats.html (CSS 재사용, HTML만 생성)
6단계: chat.html + css/pages/chat.css (채팅방)
7단계: 전체 파일을 present_files로 한 번에 제공 + 실행 방법 안내

각 단계 끝에 다음 두 가지 안내:
- "확인 방법": 이 단계가 잘 됐는지 검증하는 방법
- "다음 단계 진행 여부": "다음 단계 진행할까요?" 한 줄 질문

사용자 응답이 OK/긍정이면 즉시 다음 단계 진행. 
사용자 응답이 수정 요청이면 해당 단계만 수정 후 다시 진행 여부 확인.


[제약 사항 요약]

- HTML 4개 + CSS 7개로 구성 (단일 파일 금지)
- 외부 이미지/아이콘/폰트/JS 라이브러리 모두 금지
- 이모지 외 시각 요소는 CSS로만 표현
- 한국어 lang + 한국어 콘텐츠
- 영어 주석 금지, 한국어 주석은 허용
- 색상은 반드시 CSS 변수 var(--xxx)로 참조 (하드코딩 금지)


[즉시 진행]

위 모든 결정사항은 확정 상태. 추가 확인 질문 없이 1단계(common.css)부터
바로 시작할 것. 시작 전 "이대로 진행할까요?" 같은 메타 질문 금지.

만들어진 코드는 아래 파일과 사진으로 확인

web.zip
0.09MB

 

 

 


자바스크립트

서버 개발, 어플리케이션 개발 등 다양한 목적을 위해 사용할 수 있는 언어

웹사이트 개발에 있어 자바스크립트는 웹브라우저 및 하위 객체가 가진 기능을 구동하거나,

HTML/CSS를 통해 렌더링된 화면을 조작할 수 있다.

 

자바스크립트 작성 위치

ㄴHTML문서 내부에 작성

ㄴ자바스크립트 파일을 만들고, 그 안에 작성한 코드를 HTML 문서에 연결하기

 

DOM

웹 콘텐츠를 추가, 수정, 삭제하거나 마우스 클릭, 키보드 타이핑 등 이벤트에 대한 처리를 정의할 수 있도록 제공되는 프로그래밍 인터페이스

웹브라우저는 HTML 문서를 해석하고, 화면을 통해 해석된 결과를 보여줌

렌더링: 해석한 HTML 코드를 화면을 통해 보여주는 과정

ㄴDOM: 브라우저는 HTML 코드를 해석해서 요소들을 트리 형태로 구조화해 표현하는 문서 생성

 

document 객체 메소드

getElementById(id) id로 요소 1개 선택
getElementsByClassName(class) class로 요소들 선택 (HTMLCollection)
getElementsByTagName(tag) 태그명으로 요소들 선택 (HTMLCollection)
getElementsByName(name) name 속성으로 요소들 선택
querySelector(selector) CSS 선택자로 첫 번째 요소 선택
querySelectorAll(selector) CSS 선택자로 모든 요소 선택 (NodeList)
createElement(tag) 새 요소 생성
createTextNode(text) 텍스트 노드 생성
createDocumentFragment() 가상 컨테이너 생성 (성능 최적화용)
write(html) 문서에 HTML 직접 작성 (실무 비추천)

Element 객체 메소드 (요소 조작)

자식/형제 탐색

querySelector(selector) 자손 중 첫 번째 매칭 요소
querySelectorAll(selector) 자손 중 모든 매칭 요소
getElementsByClassName(class) 자손 중 클래스 매칭
closest(selector) 자기 자신 포함 가장 가까운 조상 찾기
contains(node) 특정 노드 포함 여부 확인
matches(selector) 선택자와 일치하는지 확인

자식 노드 조작

appendChild(node) 자식의 끝에 노드 추가
insertBefore(new, ref) ref 노드 앞에 new 노드 삽입
removeChild(node) 자식 노드 제거
replaceChild(new, old) 자식 노드 교체
append(...nodes) 끝에 노드/문자열 추가 (최신)
prepend(...nodes) 앞에 노드/문자열 추가 (최신)
before(...nodes) 자기 앞에 삽입
after(...nodes) 자기 뒤에 삽입
replaceWith(...nodes) 자기 자신 교체
remove() 자기 자신 제거

속성 조작

getAttribute(name) 속성값 가져오기
setAttribute(name, value) 속성값 설정
removeAttribute(name) 속성 제거
hasAttribute(name) 속성 존재 여부
toggleAttribute(name) 속성 토글

클래스 조작 (classList)

classList.add(class) 클래스 추가
classList.remove(class) 클래스 제거
classList.toggle(class) 클래스 토글
classList.contains(class) 클래스 포함 여부
classList.replace(old, new) 클래스 교체

이벤트

addEventListener(event, fn) 이벤트 리스너 등록
removeEventListener(event, fn) 이벤트 리스너 제거
dispatchEvent(event) 이벤트 강제 발생

위치/크기

getBoundingClientRect() 화면 기준 위치/크기 객체 반환
scrollIntoView() 해당 요소가 보이도록 스크롤
focus() 포커스 주기
blur() 포커스 해제
click() 클릭 이벤트 발생

이벤트

DOM에서 발생하는 다양한 액션 또는 상호작용 동작을 나타내는 프로그래밍 인터페이스

 

구문 기본 형태

타겟.on이벤트명 = 이벤트핸들러함수

button.onclick = handleClick

 

<form>요소

여러 입력 요소를 포함할  수 있는 폼 요소로부터 여러 입력 값을 읽어들일 때는,

개별 입력 요소의 name 속성값을 토대로 입력값에 접근할 수 있음


실습 과제 : 자바스크립트를 이용해 ‘디지털 시계’ 웹 앱 만들기

다음 링크의 페이지를 참고하여 자신만의 디지털 시계 웹 애플리케이션을 만들어 보세요.

https://js-projects-by-yoono.netlify.app/01_clock/clock

 

더보기

<role>
프론트엔드 개발 멘토로서 깔끔하고 의존성 없는 순수 HTML/CSS/JavaScript 코드를 작성할 것.
</role>

<task>
"7-세그먼트 LCD/LED 디지털 시계"처럼 보이는 웹앱 만들기
파일은 index.html, style.css, script.js 3개로 분리
외부 JS 라이브러리(jQuery, React 등) 사용 금지. 단, 웹폰트 CDN은 허용
</task>

<font_spec_CRITICAL>
폰트는 반드시 "DSEG7 Classic" (Keshikan의 7-세그먼트 디스플레이 재현 폰트)를 사용
일반 모노스페이스 폰트(Share Tech Mono, Roboto Mono, Courier 등)는 사용 금지

폰트 로딩 방법:
- cdn.jsdelivr.net에서 DSEG 폰트를 @font-face로 로드
- 예시 URL (실제 동작 확인됨):
  https://cdn.jsdelivr.net/npm/dseg@0.46.0/fonts/DSEG7-Classic/DSEG7Classic-Bold.woff2
- @font-face로 font-family: 'DSEG7Classic'을 등록하고, 시계 텍스트에 적용
- 한국어("년", "월", "일")는 DSEG에 없으므로, 한글 부분만 fallback 폰트
  (예: 'Noto Sans KR' 또는 sans-serif) 적용.
  즉 font-family는 'DSEG7Classic', 'Noto Sans KR', sans-serif 순서로 지정
</font_spec_CRITICAL>

<visual_spec>
- 전체 배경: 완전한 검정색 (#000000)
- 화면 중앙에 둥근 모서리(border-radius 16px)의 카드
  - 카드 배경: #000000
  - 카드 테두리: 1px solid #555 (희미한 회색 외곽선)
  - 카드 크기: 가로 약 520px, 세로 약 220px (반응형)
  - 카드 내부 패딩: 위아래 32px, 좌우 48px
- 카드 안 텍스트 두 줄 (모두 가로 중앙 정렬):
  - 1줄: "YYYY년 MM월 DD일"  → 폰트 크기 약 44px
  - 2줄: "HH:MM:SS"           → 폰트 크기 약 64px (시간이 더 크게 강조)
  - 줄 간격 약 16px
- 글자 색: 흰색 (#FFFFFF)

[디지털 시계 핵심 효과 — 반드시 적용]
- 7-세그먼트 "꺼진 칸" 잔상 효과:
  날짜 줄 뒤에 "8888년 88월 88일", 시간 줄 뒤에 "88:88:88"을 
  같은 위치에 겹쳐 배치하되, 색상은 rgba(255,255,255,0.08) 정도의 매우 흐린 흰색.
  → 진짜 LCD 시계에서 꺼진 세그먼트가 흐릿하게 보이는 효과 재현.
  구현: position: relative인 줄 안에 ::before 가상 요소로 "88:88:88"을 깔거나,
  실제 div 두 개를 겹쳐서 z-index로 처리.
- 글자에 미세한 발광 효과:
  text-shadow: 0 0 4px rgba(255,255,255,0.3);
  (너무 강하면 흐려보이니 약하게)
</visual_spec>

<functional_spec>
- 페이지 로딩 즉시 현재 날짜·시간 표시
- 1초마다 자동 갱신 (setInterval 1000ms)
- 월/일/시/분/초 모두 2자리 0 패딩 (예: 09:05:03)
- 사용자의 로컬 시간 기준, 24시간제
</functional_spec>

<code_quality_rules>
- 각 파일 최상단에 "이 파일이 하는 일" 한 줄 한국어 주석
- 핵심 로직(0 패딩, setInterval, 7-세그먼트 잔상 겹치기)에는 "왜 이렇게 했는지" 주석
- 변수명은 영어, 의미가 드러나게
- HTML/CSS/JS 별도 파일 분리
- 명세에 없는 기능(알람, 테마 토글 등) 추가 금지
</code_quality_rules>

<output_format>
1. 프로젝트 구조 (파일 트리)
2. index.html 전체 코드 (```html 블록)
3. style.css 전체 코드 (```css 블록)
4. script.js 전체 코드 (```javascript 블록)
5. 실행 방법 (Live Server / 더블클릭 둘 다 안내)
6. 확인 방법 체크리스트:
   - 숫자가 "각진 LCD 7-세그먼트 모양"으로 보이는가
   - 시간 뒤에 "88:88:88" 흐릿한 잔상이 보이는가
   - 1초마다 초가 갱신되는가
   - 한 자리 숫자가 두 자리(0 패딩)로 표시되는가
</output_format>

<constraints>
- 위 명세만으로 한 번에 완성
- DSEG7 Classic 폰트가 실제로 로딩되는지 @font-face URL을 정확히 작성
- 한국어 부분만 fallback 폰트로 처리하는 것이 가장 중요
</constraints>

<!-- 7-세그먼트 LCD 스타일 디지털 시계의 마크업 -->
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Digital Clock</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="clock-card">
        <div class="clock-row date-row">
            <!-- 꺼진 세그먼트 잔상: 날짜의 숫자 자리에만 8을 깔고 한글은 공백으로 두어야 위치가 정렬됨 -->
            <span class="ghost" id="dateGhost">8888년 88월 88일</span>
            <span class="active" id="dateText"></span>
        </div>
        <div class="clock-row time-row">
            <!-- 꺼진 세그먼트 잔상: 시간 자리 전체를 88:88:88로 깔아둠 -->
            <span class="ghost" id="timeGhost">88:88:88</span>
            <span class="active" id="timeText"></span>
        </div>
    </div>
    <script src="script.js"></script>
</body>
</html>
// 매초 현재 날짜와 시간을 갱신해 7-세그먼트 시계에 표시

const dateText = document.getElementById('dateText');
const timeText = document.getElementById('timeText');

// 한 자리 숫자를 두 자리로 맞추기 위한 0 패딩
// 실제 LCD 시계는 항상 고정 자리수로 표시되므로 잔상(88:88:88)과 위치가 정렬되어야 함
function pad(num) {
    return num.toString().padStart(2, '0');
}

function updateClock() {
    const now = new Date();

    const year = now.getFullYear();
    const month = pad(now.getMonth() + 1);
    const day = pad(now.getDate());

    // 24시간제 고정 — getHours()는 기본적으로 0~23 반환
    const hours = pad(now.getHours());
    const minutes = pad(now.getMinutes());
    const seconds = pad(now.getSeconds());

    dateText.textContent = `${year}년 ${month}월 ${day}일`;
    timeText.textContent = `${hours}:${minutes}:${seconds}`;
}

// 페이지 로딩 즉시 한 번 호출해야 초기 1초 동안 빈 화면이 보이지 않음
updateClock();

// setInterval은 정확히 1000ms 보장은 아니지만, 시계는 매 호출 시 Date.now()로 현재 시각을 다시 읽어오므로 누적 오차 없음
setInterval(updateClock, 1000);
/* 7-세그먼트 LCD 스타일 시계의 시각적 디자인 */

/* DSEG7 Classic 폰트를 CDN에서 직접 로드 — 숫자/콜론만 7-세그먼트 모양으로 렌더링 */
@font-face {
    font-family: 'DSEG7Classic';
    src: url('https://cdn.jsdelivr.net/npm/dseg@0.46.0/fonts/DSEG7-Classic/DSEG7Classic-Bold.woff2') format('woff2');
    font-weight: bold;
    font-style: normal;
    font-display: swap;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    min-height: 100vh;
    background-color: #000000;
    display: flex;
    justify-content: center;
    align-items: center;
}

.clock-card {
    width: 520px;
    max-width: 92vw;
    height: 220px;
    background-color: #000000;
    border: 1px solid #555;
    border-radius: 16px;
    padding: 32px 48px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 16px;
}

.clock-row {
    position: relative;
    width: 100%;
    text-align: center;
    /* DSEG7Classic을 먼저 적용해 숫자/콜론은 7-세그먼트로,
       한글(년/월/일)은 DSEG에 없으므로 Noto Sans KR로 자동 폴백 */
    font-family: 'DSEG7Classic', 'Noto Sans KR', sans-serif;
    font-weight: bold;
    color: #FFFFFF;
    text-shadow: 0 0 4px rgba(255, 255, 255, 0.3);
    white-space: nowrap;

}

.date-row {
    font-size: 44px;
}

.time-row {
    font-size: 64px;
}

/* 꺼진 세그먼트 잔상: ghost와 active를 같은 위치에 겹쳐서
   실제 LCD에서 안 켜진 8자 모양이 비치는 느낌을 재현 */
.ghost,
.active {
    display: inline-block;
}

.ghost {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    color: rgba(255, 255, 255, 0.08);
    text-shadow: none;
    z-index: 1;
}

.active {
    position: relative;
    z-index: 2;
}

 

 

다음 링크의 페이지를 참고하여 자신만의 할일 목록 웹 애플리케이션을 만들어 보세요.

https://js-projects-by-yoono.netlify.app/03_todo/todo

 

더보기

<task>
바닐라 HTML/CSS/JavaScript로 분리된 3개 파일(index.html, style.css, script.js) 구조의
할 일 목록(Todo) 웹 애플리케이션 한 세트를 생성할 것.
프레임워크, 빌드 도구, 외부 CDN은 사용하지 말 것.
같은 폴더에 세 파일을 두고 index.html을 통해 즉시 동작해야 함.

기능은 아래 3가지.

그 외 기능(필터, 완료 체크, 정렬, 카테고리, 우선순위, 마감일, 편집 등)은 추가하지 말 것.

1. 추가: 입력창에 텍스트 입력 후 "추가" 버튼 또는 Enter 키로 항목 생성.
        빈 문자열 및 공백만 있는 입력은 무시. 추가 후 입력창 자동 비움.
2. 삭제: 각 항목의 × 버튼 클릭 시 해당 항목만 제거.
3. 영구 저장: localStorage에 목록 저장. 새로고침/재방문 후에도 유지.
</task>

<context>
- 대상 LLM: Claude Opus 4.7 (Artifact로 산출하지 말고 파일별 코드 블록으로 출력)
- 사용자 수준: 코딩 입문자. 코드는 그대로 복사해 폴더에 저장하면 바로 동작해야 함
- 실행 환경: 최신 데스크톱/모바일 브라우저 (Chrome, Safari, Edge 최신 버전)
- 코드 주석 언어: 한국어. "무엇을 하는지"보다 "왜 이렇게 했는지"를 적기
- 접근성 최소 요건: 입력창에 aria-label, 삭제 버튼에 aria-label 부여
- 반응형: 모바일 폭 약 380px에서 레이아웃이 깨지지 않을 정도면 충분
</context>

<references>
[디자인 방향]
아래 무드보드를 기준으로 디자인을 일관되게 적용할 것.

전체 분위기:
- 다크 모드 기반, 차분하고 기술적인 인터페이스
- 코드 에디터나 개발자 도구를 연상시키는 정돈된 느낌
- 장식 최소화. 타이포그래피와 여백으로 위계 표현

색상 팔레트:
- 배경: 깊은 네이비/차콜 (#0a0e1a ~ #0f1419)
- 카드/패널: 배경보다 살짝 밝은 톤 (#151b2e 정도) + 1px 얇은 보더 (#222a3d)
- 텍스트 메인: 밝은 회백색 (#e4e7ec)
- 텍스트 보조: 중간 회색 (#8b95a7)
- 액센트: 시안 #00d4ff (focus ring, hover 보더, 강조선에만 절제 사용)

타이포그래피:
- UI 본문: 시스템 산세리프 (-apple-system, "Segoe UI", sans-serif)
- 제목, placeholder, 빈 상태 문구: 모노스페이스 (ui-monospace, "SF Mono", Menlo, monospace)
- 제목은 letter-spacing 살짝 (0.05em 정도)로 단단한 인상

형태와 디테일:
- border-radius 4~8px (직각에 가깝게)
- 그림자는 매우 옅게 또는 미사용
- 카드/입력창에 1px 얇은 보더
- 버튼 hover 시 보더 색이 액센트로 부드럽게 전환 (transition 150ms ease)
- 배경에 아주 옅은 도트 또는 그리드 패턴 허용 (opacity 0.03~0.05, 순수 CSS로)

레이아웃:
- 중앙 정렬, 카드 최대 너비 480px
- 상단: 제목 (예: "TODO" 같은 짧고 단단한 모노스페이스 표기)
- 중단: 할 일 리스트
- 하단: 입력창 + 추가 버튼 (가로 배치)
- 리스트가 비었을 때: 모노스페이스로 "// no tasks" 같은 placeholder 문구 표시

[출력 형식 예시]
파일별로 다음과 같이 코드 블록을 나눌 것:

### index.html
```html
<!-- 코드 -->
```

### style.css
```css
/* 코드 */
```

### script.js
```javascript
// 코드
```

마지막에 "사용법" 한 단락 (저장 위치 + 여는 방법).
</references>

<evaluate>
산출물은 다음 기준을 모두 만족해야 함.

기능 정확성:
- 추가/삭제/저장 3가지가 위 명세대로 정확히 동작
- 빈 입력/공백 입력은 추가되지 않음
- 새로고침 후 목록이 그대로 복원됨
- 명세에 없는 기능은 코드에 포함되지 않음

코드 품질:
- 외부 의존성 0개 (CDN, 폰트, 아이콘 라이브러리 일체 없음)
- 한 파일 안에 다른 파일 코드가 섞이지 않음 (HTML에 <style>/<script> 인라인 금지)
- 핵심 로직에 의도 중심 한국어 주석

디자인 일관성:
- 위 색상 팔레트와 타이포그래피가 그대로 반영됨
- 노란 말풍선 톤이나 파스텔 톤이 들어오지 않음
- 모바일 380px 폭에서 가로 스크롤 발생하지 않음
</evaluate>

<iterate>
한 번에 완성된 세트로 출력할 것.
추가 질문하지 말고, 명세 안에서 합리적으로 결정한 뒤 그 결정은 채팅 요약으로 제공할 것.
</iterate>

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TODO</title>
  <link href="style.css" rel="stylesheet">
</head>
<body>
  <main class="card">
    <h1 class="title">TODO</h1>

    <ul class="list" id="list"></ul>

    <p class="empty" id="empty">// no tasks</p>

    <div class="input-row">
      <input
        class="input"
        id="input"
        type="text"
        placeholder="// new task"
        aria-label="할 일 입력"
        autocomplete="off"
      >
      <button class="add-btn" id="addBtn" type="button">추가</button>
    </div>
  </main>

  <script src="script.js"></script>
</body>
</html>
/* 다크 코드 에디터 톤의 톤 차분한 Todo UI 스타일 */

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:root {
  --bg: #0a0e1a;
  --panel: #151b2e;
  --border: #222a3d;
  --text: #e4e7ec;
  --text-sub: #8b95a7;
  --accent: #00d4ff;
  --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

html, body {
  min-height: 100vh;
  background-color: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 14px;
}

/* 배경 도트 패턴은 시각적 노이즈를 최소화하기 위해 opacity 0.04로 매우 옅게 */
body {
  background-image: radial-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px);
  background-size: 18px 18px;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 80px 16px 40px;
}

.card {
  width: 100%;
  max-width: 480px;
  background-color: var(--panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 28px;
}

.title {
  font-family: var(--font-mono);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: 0.05em;
  color: var(--text);
  margin-bottom: 20px;
}

.list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 16px;
}

.list:empty {
  display: none;
}

.item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 14px;
  background-color: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  transition: border-color 150ms ease;
}

.item:hover {
  border-color: var(--accent);
}

.item-text {
  flex: 1;
  word-break: break-word;
  color: var(--text);
}

.delete-btn {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  background-color: transparent;
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text-sub);
  font-family: var(--font-mono);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  transition: border-color 150ms ease, color 150ms ease;
}

.delete-btn:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.delete-btn:focus-visible {
  outline: none;
  border-color: var(--accent);
  color: var(--accent);
}

.empty {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text-sub);
  padding: 10px 14px;
  margin-bottom: 16px;
}

.empty.hidden {
  display: none;
}

.input-row {
  display: flex;
  gap: 8px;
  margin-top: 4px;
}

.input {
  flex: 1;
  min-width: 0;
  background-color: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 10px 14px;
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 14px;
  transition: border-color 150ms ease;
}

.input::placeholder {
  color: var(--text-sub);
  font-family: var(--font-mono);
}

.input:focus {
  outline: none;
  border-color: var(--accent);
}

.add-btn {
  flex-shrink: 0;
  padding: 0 18px;
  background-color: transparent;
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: 0.05em;
  cursor: pointer;
  transition: border-color 150ms ease, color 150ms ease;
}

.add-btn:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.add-btn:focus-visible {
  outline: none;
  border-color: var(--accent);
  color: var(--accent);
}

@media (max-width: 420px) {
  body {
    padding: 40px 12px 24px;
  }

  .card {
    padding: 20px;
  }

  .add-btn {
    padding: 0 14px;
  }
}
// 추가/삭제/localStorage 영구 저장 3가지 기능만 담당하는 Todo 로직

const STORAGE_KEY = "todo-items";

const listEl = document.getElementById("list");
const emptyEl = document.getElementById("empty");
const inputEl = document.getElementById("input");
const addBtnEl = document.getElementById("addBtn");

// 메모리 상태를 단일 진실 원천으로 두고, 변경 시마다 localStorage와 DOM을 동기화
let items = loadItems();

function loadItems() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return [];
    const parsed = JSON.parse(raw);
    return Array.isArray(parsed) ? parsed : [];
  } catch {
    // 저장 데이터가 손상된 경우 빈 배열로 복구해서 앱이 멈추지 않게 함
    return [];
  }
}

function saveItems() {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
}

// 시간 + 랜덤값 조합으로 외부 라이브러리 없이 충돌 가능성 거의 0인 ID 생성
function createId() {
  return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}

function render() {
  listEl.innerHTML = "";

  if (items.length === 0) {
    emptyEl.classList.remove("hidden");
  } else {
    emptyEl.classList.add("hidden");
  }

  for (const item of items) {
    const li = document.createElement("li");
    li.className = "item";
    li.dataset.id = item.id;

    const span = document.createElement("span");
    span.className = "item-text";
    span.textContent = item.text;

    const btn = document.createElement("button");
    btn.className = "delete-btn";
    btn.type = "button";
    btn.textContent = "×";
    btn.setAttribute("aria-label", `${item.text} 삭제`);

    li.appendChild(span);
    li.appendChild(btn);
    listEl.appendChild(li);
  }
}

function addItem() {
  // trim 결과가 빈 문자열이면 공백만 입력된 경우이므로 추가 차단
  const text = inputEl.value.trim();
  if (text === "") return;

  items.push({ id: createId(), text });
  saveItems();
  render();

  inputEl.value = "";
  inputEl.focus();
}

function deleteItem(id) {
  items = items.filter((item) => item.id !== id);
  saveItems();
  render();
}

addBtnEl.addEventListener("click", addItem);

// 한글 IME 조합 중 Enter는 글자 확정용이라 추가 동작이 일어나면 안 됨 (isComposing 체크)
inputEl.addEventListener("keydown", (e) => {
  if (e.key === "Enter" && !e.isComposing) {
    e.preventDefault();
    addItem();
  }
});

// 항목이 동적으로 늘어나도 매번 리스너 다는 비용을 피하기 위해 ul에 이벤트 위임
listEl.addEventListener("click", (e) => {
  const btn = e.target.closest(".delete-btn");
  if (!btn) return;
  const li = btn.closest(".item");
  if (!li) return;
  deleteItem(li.dataset.id);
});

render();