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 | 클릭 가능한 버튼 (폼 제출·이벤트 트리거) |
- 조사한 내용을 바탕으로, 다음과 같은 형태의 페이지를 만들어 보세요. 인물은 자유롭게 변경 가능.
- 다음 페이지를 만든 프롬프트를 정리해서 메모장에 저장해두세요! 제가 확인할 겁니다.
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)부터
바로 시작할 것. 시작 전 "이대로 진행할까요?" 같은 메타 질문 금지.
만들어진 코드는 아래 파일과 사진으로 확인




자바스크립트
서버 개발, 어플리케이션 개발 등 다양한 목적을 위해 사용할 수 있는 언어
웹사이트 개발에 있어 자바스크립트는 웹브라우저 및 하위 객체가 가진 기능을 구동하거나,
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();

'로보테크AI' 카테고리의 다른 글
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/05/19[멘토링] (0) | 2026.05.19 |
|---|---|
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/05/18 (0) | 2026.05.18 |
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/05/13[심화 프로젝트 발표회] (0) | 2026.05.13 |
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/05/12 (0) | 2026.05.12 |
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/05/11 (0) | 2026.05.11 |