<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ttt0_0.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Wed, 27 May 2026 11:42:55 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ttt0_0.log</title>
            <url>https://velog.velcdn.com/images/ttt0_0/profile/d7470b3d-1d0d-42f5-8181-650c68e70cde/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ttt0_0.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ttt0_0" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Flutter 10일차💕] 일정 삭제 기능과 일정 개수 카운트 하기]]></title>
            <link>https://velog.io/@ttt0_0/Flutter-10%EC%9D%BC%EC%B0%A8-%EC%9D%BC%EC%A0%95-%EC%82%AD%EC%A0%9C-%EA%B8%B0%EB%8A%A5%EA%B3%BC-%EC%9D%BC%EC%A0%95-%EC%B9%B4%EC%9A%B4%ED%8A%B8-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ttt0_0/Flutter-10%EC%9D%BC%EC%B0%A8-%EC%9D%BC%EC%A0%95-%EC%82%AD%EC%A0%9C-%EA%B8%B0%EB%8A%A5%EA%B3%BC-%EC%9D%BC%EC%A0%95-%EC%B9%B4%EC%9A%B4%ED%8A%B8-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 27 May 2026 11:42:55 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h2 id="오늘의-목표--일정-삭제-기능과-0개로-고정되어-있는-일정-개수-실시간-데이터-개수로-업데이트하기">오늘의 목표 : 일정 삭제 기능과 0개로 고정되어 있는 일정 개수 실시간 데이터 개수로 업데이트하기</h2>
</blockquote>
<h2 id="핵심-개념">핵심 개념</h2>
<blockquote>
<p><strong>createState()</strong> : 위젯이 빌드할 때 호출되는 StatefulWidget 사용 시 필수 구현하는 메소드
<strong>DateTime</strong> : 날짜, 시간 제공하는 클래스
<strong>Scaffold</strong> : 기본 뼈대 위젯
<strong>floatingActionButton</strong> : 우측 하단에 떠있는 버튼
<strong>⭐️StreamBuilder⭐️</strong> : 실시간 데이터를 화면에 업데이트할 때 사용하는 위젯</p>
</blockquote>
<h3 id="수업-코드">수업 코드</h3>
<p><strong>/screen/home_screen.dart</strong></p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:calendar_scheduler/component/main_calendar.dart&#39;;
import &#39;package:calendar_scheduler/component/schedule_card.dart&#39;;
import &#39;package:calendar_scheduler/component/today_banner.dart&#39;;
import &#39;package:calendar_scheduler/component/schedule_bottom_sheet.dart&#39;;
import &#39;package:calendar_scheduler/const/colors.dart&#39;;
import &#39;package:get_it/get_it.dart&#39;;
import &#39;package:calendar_scheduler/database/drift_database.dart&#39;;


class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  State&lt;HomeScreen&gt; createState() =&gt; _HomeScreenState();
}

class _HomeScreenState extends State&lt;HomeScreen&gt;{
  DateTime selectedDate = DateTime.utc(

    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        backgroundColor: PRIMARY_COLOR,
        onPressed: (){
          showModalBottomSheet(
              context: context,
              isDismissible : true,
              builder: (_) =&gt;ScheduleBottomSheet(
                selectedDate: selectedDate,
              ),
              isScrollControlled: true, // 화면 최대 높이를 : 화면 전체로 변경

          );
        },
        child: Icon(
          Icons.add,
        ),
      ),

      body: SafeArea(
          child: Column(
            children: [
              MainCalendar(
                selectedDate: selectedDate,
                //선택된 날짜 전달 코드

                onDaySelected: onDaySelected,
              ),
              SizedBox(height:8),
              StreamBuilder&lt;List&lt;Schedule&gt;&gt;(
                stream: GetIt.I&lt;LocalDatabase&gt;().watchSchedules(selectedDate),
                builder: (context, snapshoot){
                  return TodayBanner(
                      selectedDate: selectedDate,
                      count: snapshoot.data?.length ?? 0,
                  );
                },
              ),
              SizedBox(height:8),

              Expanded( //컬럼에서 남은 공간 전체를 차지하게 하는 공간
                  child: StreamBuilder&lt;List&lt;Schedule&gt;&gt;(  // 스트림빌더 : 바뀔때마다 알려줌
                    //스트림한 데이터를
                    // 스트림: 값 분해한 것을 ..
                    stream: GetIt.I&lt;LocalDatabase&gt;().watchSchedules(selectedDate),
                    // 데이터 베이스에서 가
                    // 새로 추가되거나 수정 되면 알려달라고 함
                    builder: (context, snapshot){
                      // snapshot : 데이터 와쓴ㄴ지, 에러 어떤건지
                      if (!snapshot.hasData) {
                        return Container(); // 없으면 : 빈화면
                      }
                      return ListView.builder(
                        itemCount:snapshot.data!.length,
                        itemBuilder: (context, index){
                          final schedule = snapshot.data![index]; // 일정 목록이 들어옴
                          return Dismissible(
                            key:ObjectKey(schedule.id), //유니크한 키값
                            direction:DismissDirection.startToEnd,
                            onDismissed: (DismissDirection direction) {
                            GetIt.I&lt;LocalDatabase&gt;()
                                .removeSchedule(schedule.id);
                            },
                            child: Padding(
                              padding: const EdgeInsets.only(bottom: 8, left:8,
                                  right: 8),
                              child: ScheduleCard(
                                  startTime: schedule.startTime,
                                  endTime: schedule.endTime,
                                  content: schedule.content
                              ),

                            ),
                          );
                        },
                      );
                    },
                  )
              )
            ],
          ),
      ),
    );
  }

  void onDaySelected(DateTime selectedDate, DateTime focusedDate) {
    // 날짜 선택될 때마다 실행할 함수
    setState(() {
      this.selectedDate = selectedDate;
    });

  }
}</code></pre>
<p>=&gt; 날짜 선택, 일정 추가,삭제,목록표시 담당하는 앱의 메인 화면</p>
<h3 id="코드-분석">코드 분석</h3>
<p><strong>/screen/home_screen.dart</strong></p>
<pre><code class="language-dart">return Dismissible(
                            key:ObjectKey(schedule.id), //유니크한 키값
                            direction:DismissDirection.startToEnd,
                            onDismissed: (DismissDirection direction) {
                            GetIt.I&lt;LocalDatabase&gt;()
                                .removeSchedule(schedule.id);
                            },
                            child: Padding(
                              padding: const EdgeInsets.only(bottom: 8, left:8,
                                  right: 8),
                              child: ScheduleCard(
                                  startTime: schedule.startTime,
                                  endTime: schedule.endTime,
                                  content: schedule.content
                              ),

                            ),
                          );

GetIt.I&lt;LocalDatabase&gt;()
    .removeSchedule(schedule.id); //실제 삭제 기능 schedule.id 고유한 번호
},

StreamBuilder&lt;List&lt;Schedule&gt;&gt;(
                stream: GetIt.I&lt;LocalDatabase&gt;().watchSchedules(selectedDate),
                builder: (context, snapshoot){
                  return TodayBanner(
                      selectedDate: selectedDate,
                      count: snapshoot.data?.length ?? 0,
                  );
                },
              ),</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>Dismissible</td>
<td>일정 삭제</td>
<td>일정을 왼쪽에서 오른쪽으로 밀었을 때 일정 삭제</td>
</tr>
<tr>
<td>direction:DismissDirection.startToEnd</td>
<td>일정 삭제</td>
<td>미는 방향 지정</td>
</tr>
<tr>
<td>removeSchedule(schedule.id)</td>
<td>삭제 조건</td>
<td>DB에서 주는 고유한 번호를 지정하여 해당 번호 일정 삭제</td>
</tr>
<tr>
<td>count: snapshoot.data?.length ?? 0</td>
<td>일정 개수 세기</td>
<td>DB에서 가져온 일정 목록이 null이 아니면 개수 가져옴, null이면 0으로 표시</td>
</tr>
</tbody></table>
<h3 id="코드-동작-순서">코드 동작 순서</h3>
<blockquote>
<ol>
<li>selectedDate를 오늘 날짜로 초기화</li>
<li>화면 렌더링(화면 그리기)</li>
<li>달력에서 날짜 선택</li>
<li>onDaySelected 실행 → setState로 selectedDate 업데이트
 → MainCalendar 선택 날짜 파란색으로
 → TodayBanner 날짜 바뀜
 → StreamBuilder가 새 날짜 일정 자동으로 불러옴</li>
<li>일정 추가 버튼 누름
ScheduleBottomSheet 올라옴
→ 시작시간/종료시간/내용 입력
→ 저장 버튼 클릭 → DB에 저장
→ StreamBuilder 변화 감지
→ ScheduleCard 목록에 자동 추가
→ TodayBanner 개수 +1 카운트</li>
<li>일정 삭제 
ScheduleCard 오른쪽으로 밀기
→ onDismissed 실행
→ removeSchedule(schedule.id)로 DB에서 삭제
→ StreamBuilder 변화 감지
→ 목록에서 자동으로 사라짐
→ TodayBanner 개수 -1 줄음</li>
</ol>
</blockquote>
<h3 id="코드-직접-변경-후-적용">코드 직접 변경 후 적용</h3>
<pre><code class="language-dart">direction:DismissDirection.endToStart,</code></pre>
<p>방향을 startToEnd에서 endToStart, up, down으로 바꿔서 삭제하기 위해 일정 카드를 미는 방향을 바꿔봤다.</p>
<table>
<thead>
<tr>
<th>코드</th>
<th>미는 방향</th>
</tr>
</thead>
<tbody><tr>
<td>startToEnd</td>
<td>왼쪽 -&gt; 오른쪽</td>
</tr>
<tr>
<td>endToStart</td>
<td>오른쪽 -&gt; 왼쪽</td>
</tr>
<tr>
<td>up</td>
<td>아래 -&gt; 위</td>
</tr>
<tr>
<td>down</td>
<td>위 -&gt; 아래</td>
</tr>
</tbody></table>
<h3 id="에러---해결-방법">에러  &amp; 해결 방법</h3>
<blockquote>
<p><strong>🚨에러🚨</strong> : ❌</p>
</blockquote>
<h3 id="새로-알게된-것">새로 알게된 것</h3>
<blockquote>
<p><strong>StreamBuilder *<em>개념을 새로 배웠다. StreamBuilder는 데이터가 계속 변경되기 때문에 사용한다. Stream에 데이터가 들어올 때마다 builder를 실행한다. *</em>snapshot</strong>은 들어온 데이터를 가지고 있다가 StreamBuilder가 새로운 데이터를 가지고 오면 새로운 데이터로 갱신하여 가지로 있는다. snapshot은 데이터뿐만 아니라 데이터 상태도 가지고 있는다. (hasError, hasData, data)</p>
</blockquote>
<h3 id="결과물">결과물</h3>
<p>일정 삭제
<img src="https://velog.velcdn.com/images/ttt0_0/post/9b2061ec-8a08-4c11-9198-78b42af7622b/image.png" alt="">
일정 개수 실시간 데이터로 반영
<img src="https://velog.velcdn.com/images/ttt0_0/post/daf50fec-409c-4a8a-8b06-8d5cc4b7c3d4/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter 9일차] 캘린더에 일정 추가 후 저장]]></title>
            <link>https://velog.io/@ttt0_0/Flutter-9%EC%9D%BC%EC%B0%A8-%EC%BA%98%EB%A6%B0%EB%8D%94%EC%97%90-%EC%9D%BC%EC%A0%95-%EC%B6%94%EA%B0%80-%ED%9B%84-%EC%A0%80%EC%9E%A5</link>
            <guid>https://velog.io/@ttt0_0/Flutter-9%EC%9D%BC%EC%B0%A8-%EC%BA%98%EB%A6%B0%EB%8D%94%EC%97%90-%EC%9D%BC%EC%A0%95-%EC%B6%94%EA%B0%80-%ED%9B%84-%EC%A0%80%EC%9E%A5</guid>
            <pubDate>Mon, 25 May 2026 08:00:05 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘의-목표--캘린더에-일정-날짜-시작종료-시간-내용-넣어서-저장하기">오늘의 목표 : 캘린더에 일정 날짜, 시작/종료 시간, 내용 넣어서 저장하기</h2>
<h2 id="핵심-개념">핵심 개념</h2>
<blockquote>
<p><strong>Widget</strong> : Flutter 앱의 UI를 구성하는 가장 기본적이고 핵심적인 요소
<strong>생성자 안 required 변수</strong> : 반드시 사용해야하는 변수
<strong>Column</strong> : 세로 방향으로 배치하는 기본 레이아웃 위젯
<strong>validator</strong> : 입력값 체크
<strong>StatefulWidget</strong> : 값이 변경되어 적용할 때 사용하는 위젯
<strong>StatelessWidget</strong> : 값이 변경되지 않는 화면의 위젯
<strong>async</strong> : 앱이 멈추지 않고 비동기 처리를 기다리게 하는 키워드
<strong>await</strong> : DB 저장이 끝날때까지 기다리고 실행하는 키워드
<strong>Expanded</strong> : 컬럼에서 남은 공간 전체를 차지하게 하는 공간
<strong>child</strong> : 부모 위젯 내부에 단 하나의 자식 위젯을 포함할 때 사용하는 속성</p>
</blockquote>
<h3 id="수업-코드">수업 코드</h3>
<p><strong>/component/custom_text_field.dart</strong></p>
<pre><code class="language-dart">import &#39;package:calendar_scheduler/const/colors.dart&#39;;
import &#39;package:flutter/material.dart&#39;;
import &#39;package:flutter/services.dart&#39;;

class CustomTextField extends StatelessWidget {
  //외부에서 받을 값 final
  final String label;
  final bool isTime; // 시간 입력용 True일때- 시간 입력  false - 일반 텍스트 입력

  final FormFieldSetter&lt;String&gt; onSaved; // 입력값 저장하는 변수
  final FormFieldValidator&lt;String&gt; validator; //입력값 체크하는 변수

  //생성자
  const CustomTextField({
    required this.label,
    required this.isTime,
    required this.onSaved,
    required this.validator,

    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          label,
          style: TextStyle(
            color: PRIMARY_COLOR,
            fontWeight: FontWeight.w600,
          ),
        ),
        Expanded(
          flex: isTime ? 0 : 1,
          child: TextFormField(

            onSaved: onSaved, // 입력값 최종 저장 시 실행
            validator: validator, // 입력값 체크


            cursorColor: Colors.grey,
            // isTime = true : 한 줄 입력
            maxLines: isTime ? 1 : null,
            // isTime = false : 여러 줄 입력 (박스 형태로 늘어남)
            expands: !isTime,
            keyboardType:
            isTime ? TextInputType.number : TextInputType.multiline,
            // TextInputType.number : 숫자 키보드
            // TextInputType.multiline : 일반 키보드
            inputFormatters: isTime
                ? [FilteringTextInputFormatter.digitsOnly] // 숫자만 입력가능하게 함
                : [],
            decoration: InputDecoration(
              border: InputBorder.none,
              filled: true,
              fillColor: Colors.grey[300],
              //
              suffixText: isTime ? &#39;시&#39; : null,
            ),
          ),
        ),
      ],
    );
  }
}</code></pre>
<p>=&gt;입력칸을 재사용 가능하게 만드는 코드</p>
<p><strong>component/main_calendar.dart</strong></p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:table_calendar/table_calendar.dart&#39;;
import &#39;package:calendar_scheduler/const/colors.dart&#39;;


class MainCalendar extends StatelessWidget{
  final OnDaySelected onDaySelected; //날짜 선택시 실행
  final DateTime selectedDate; //선택된 날짜

  const MainCalendar({
    required this.onDaySelected,
    required this.selectedDate,
  });

  @override
  Widget build(BuildContext context) {
    return TableCalendar(
      locale: &#39;ko_kr&#39;,
      onDaySelected: onDaySelected,
      selectedDayPredicate: (date)=&gt; //선택 된 날짜 구분 로직
      date.year == selectedDate.year&amp;&amp;
      date.month == selectedDate.month&amp;&amp;
      date.day == selectedDate.day,

      firstDay: DateTime(1800, 1, 1), //첫째날
      lastDay : DateTime(3000, 1, 11), //마지막 날
      focusedDay: DateTime.now(), //화면에 보여지는 날

      headerStyle: HeaderStyle(
        titleCentered: true,
        formatButtonVisible: false,
        titleTextStyle: TextStyle(
          fontWeight: FontWeight.w700,
          fontSize: 16.0,
        ),

      ),
      calendarStyle:CalendarStyle(
        isTodayHighlighted: false,
        defaultDecoration: BoxDecoration(
          borderRadius: BorderRadius.circular(6.0),
          color:LIGHT_GREY_COLOR
        ),
          weekendDecoration: BoxDecoration(
              borderRadius: BorderRadius.circular(6.0),
              color:LIGHT_GREY_COLOR
          ),
          selectedDecoration: BoxDecoration(
              borderRadius: BorderRadius.circular(6.0),
              border: Border.all(
                color: PRIMARY_COLOR,
                width: 1.0,
              ),
          ),
        defaultTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: DARK_GREY_COLOR,
        ),
        weekendTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: DARK_GREY_COLOR,
        ),
        selectedTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: PRIMARY_COLOR,
        ),
      ),

    );
  }


}</code></pre>
<p>=&gt; table_calendar 패키지 이용해서 한국어로 날짜 선택 가능한 캘린더 UI를 만든 위젯</p>
<p><strong>component/schedule_bottom_sheet.dart</strong></p>
<pre><code class="language-dart">import &#39;package:drift/drift.dart&#39; hide Column;
import &#39;package:get_it/get_it.dart&#39;;
import &#39;package:calendar_scheduler/database/drift_database.dart&#39;;

import &#39;package:flutter/material.dart&#39;;
import &#39;package:calendar_scheduler/const/colors.dart&#39;;
import &#39;package:calendar_scheduler/component/custom_text_field.dart&#39;;

//일정 등록 시 올라오는 창
// 입력값을 저장하기 위해 StatefulWidget으로 함
class ScheduleBottomSheet extends StatefulWidget{
  final DateTime selectedDate;


  const ScheduleBottomSheet({
    required this.selectedDate,
    Key? key}) : super(key:key);

  @override
  State&lt;ScheduleBottomSheet&gt; createState() =&gt; _ScheduleBottomSheetState();
}

class _ScheduleBottomSheetState extends State&lt;ScheduleBottomSheet&gt; {
  final GlobalKey&lt;FormState&gt; formKey = GlobalKey(); // 폼Key, 폼 조종 리모컨

  int? startTime; // 입력값 저장 변수
  int? endTime; // ? : null일 수도 있어서
  String? content;

  @override
  Widget build(BuildContext context) {
    //키보드 높이 가져오기
    final bottomInset = MediaQuery
        .of(context)
        .viewInsets
        .bottom;
    //viewInsets : 시스템이 차지하는 화면의 bottom: 아랫부분 크기를 알수 있음

    // *****중요
    // 저장 버튼 눌렀을 때 전체 입력한 값을 관리할 수 있도록 Form()으로 감쌈
    return Form(
      key: formKey,
      child: SafeArea(
        child: Container(
          // MediaQueㄴry : SafeArea에 화면의 반 차지하는 컨테이너 위젯을 배치
          height: MediaQuery.of(context).size.height / 2 + bottomInset,
          color: Colors.white,
          child: Padding(
            padding: const EdgeInsets.all(8),
            child: Column(
              children: [
                Row(
                  children: [
                    Expanded(
                      child: CustomTextField(
                        label: &#39;시작시간&#39;,
                        isTime: true,
                        onSaved: (String? val) {
                          startTime = int.parse(val!);
                          // 모든 입력값은 문자여서 숫자로 변환함
                        },
                        validator: timeValidator,
                      ),
                    ),
                    Expanded(
                      child: CustomTextField(
                        label: &#39;종료시간&#39;,
                        isTime: true,
                        onSaved: (String? val) {
                          endTime = int.parse(val!);
                        },
                        validator: timeValidator,
                      ),
                    ),
                    const SizedBox(width: 16),
                  ],
                ),
                SizedBox(height: 8),
                SizedBox(
                  height: 120,
                  child: CustomTextField(
                    label: &#39;내용&#39;,
                    isTime: false,
                    onSaved: (String? val) {
                      content = val;
                    },
                    validator: contentValidator,
                  ),
                ),
                SizedBox(
                  width: double.infinity,
                  child: ElevatedButton(
                    onPressed: onSavePressed,
                    style: ElevatedButton.styleFrom(foregroundColor: PRIMARY_COLOR),
                    child: Text(&#39;저장&#39;),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  // 저장 버튼 눌렀을때
  // async : 시간을 기다릴 수 ㅣㅇ있게 기다리는
  void onSavePressed() async {
    if (formKey.currentState!.validate()) {
      //validate -&gt; 하나라도 문제있으면 false qksghks
      // 전체 통과 -&gt; true
      formKey.currentState!.save();
      // -&gt; true일때 실행
      // ! -&gt; null이 아니라고 확신할 때 dart에서 사용
      // formKey는 이미 연결을 해서 null이 아님
      // 현재 상태가 있음을 기본 베이스로 생각하기

      // await -&gt; DB 저장이 끝날때까지 기다리고 실행
      await GetIt.I&lt;LocalDatabase&gt;().createSchedule(
        SchedulesCompanion(
          startTime: Value(startTime!),
          endTime: Value(endTime!),
          content: Value(content!),
          date: Value(widget.selectedDate),
        ),
      );

      Navigator.of(context).pop();
    }
  }

  //폼키 실행
  String? timeValidator(String? val) {
    if(val==null) {
      return &#39;값을 입력하세요!&#39;;
    }

    int? number;
    try {
      number = int.parse(val);
    } catch (e) {
      return &quot;숫자만 입력하세요.&quot;;
    }

    if(number &lt; 0 || number &gt; 24) {
      return &#39;0~24 사이를 입력하세요!&#39;;
    }

    return null;
  } // 시간 값 검증


  String? contentValidator(String? val){
    if(val == null || val.length == 0) {
      return &#39;값을 입력하세요!&#39;;
    }
    return null;
  } // 내용 값 검증
}
</code></pre>
<p>=&gt; 일정 추가 버튼 눌렀을 때 나오는 입력창 입력칸 내용 유효성 검사 후 DB에 저장하는 위젯</p>
<p><strong>screen/home_screen.dart</strong></p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:calendar_scheduler/component/main_calendar.dart&#39;;
import &#39;package:calendar_scheduler/component/schedule_card.dart&#39;;
import &#39;package:calendar_scheduler/component/today_banner.dart&#39;;
import &#39;package:calendar_scheduler/component/schedule_bottom_sheet.dart&#39;;
import &#39;package:calendar_scheduler/const/colors.dart&#39;;
import &#39;package:get_it/get_it.dart&#39;;
import &#39;package:calendar_scheduler/database/drift_database.dart&#39;;


class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  State&lt;HomeScreen&gt; createState() =&gt; _HomeScreenState();
}

class _HomeScreenState extends State&lt;HomeScreen&gt;{
  DateTime selectedDate = DateTime.utc(

    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        backgroundColor: PRIMARY_COLOR,
        onPressed: (){
          showModalBottomSheet(
              context: context,
              isDismissible : true,
              builder: (_) =&gt;ScheduleBottomSheet(
                selectedDate: selectedDate,
              ),
              isScrollControlled: true, // 화면 최대 높이를 : 화면 전체로 변경

          );
        },
        child: Icon(
          Icons.add,
        ),
      ),

      body: SafeArea(
          child: Column(
            children: [
              MainCalendar(
                selectedDate: selectedDate,
                //선택된 날짜 전달 코드

                onDaySelected: onDaySelected,
              ),
              SizedBox(height:8),
              TodayBanner(
                  selectedDate: selectedDate,
                  count: 0
              ),
              SizedBox(height:8),

              Expanded( //컬럼에서 남은 공간 전체를 차지하게 하는 공간
                  child: StreamBuilder&lt;List&lt;Schedule&gt;&gt;(  // 스트림빌더 : 바뀔때마다 알려줌
                    //스트림한 데이터를
                    // 스트림: 값 분해한 것을 ..
                    stream: GetIt.I&lt;LocalDatabase&gt;().watchSchedules(selectedDate),
                    // 데이터 베이스에서 가
                    // 새로 추가되거나 수정 되면 알려달라고 함
                    builder: (context, snapshot){
                      // snapshot : 데이터 와쓴ㄴ지, 에러 어떤건지
                      if (!snapshot.hasData) {
                        return Container(); // 없으면 : 빈화면
                      }
                      return ListView.builder(
                        itemCount:snapshot.data!.length,
                        itemBuilder: (context, index){
                          final schedule = snapshot.data![index]; // 일정 목록이 들어옴
                          return Padding(
                            padding: const EdgeInsets.only(bottom: 8, left:8,
                                                           right: 8),
                            child: ScheduleCard(
                                startTime: schedule.startTime,
                                endTime: schedule.endTime,
                                content: schedule.content
                            ),

                            );
                        },
                      );
                    },
                  )
              )
            ],
          ),
      ),
    );
  }

  void onDaySelected(DateTime selectedDate, DateTime focusedDate) {
    // 날짜 선택될 때마다 실행할 함수
    setState(() {
      this.selectedDate = selectedDate;
    });

  }
}</code></pre>
<p>=&gt; 메인 화면, 전체 화면을 보여주는 위젯</p>
<h3 id="코드-분석">코드 분석</h3>
<p><strong>component/custom_text_field.dart</strong></p>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>final 변수</td>
<td>외부에서 받을 값을 저장</td>
<td></td>
</tr>
<tr>
<td>required 변수</td>
<td>CustomTextField를 선언했을 때 반드시 사용해야하는 변수</td>
<td></td>
</tr>
<tr>
<td>CrossAxisAlignment.start</td>
<td>반대축의 시작점에 정렬</td>
<td>Column(세로) -&gt; 가로 왼쪽, Row(가로) -&gt; 세로 맨위</td>
</tr>
<tr>
<td>onSaved</td>
<td>입력값 최종 저장 시 실행</td>
<td></td>
</tr>
<tr>
<td>isTime ? TextInputType.number : TextInputType.multiline</td>
<td>isTime 값에 따라 다른 키보드형태</td>
<td>true : 숫자 키보드, false : 일반 키보드</td>
</tr>
</tbody></table>
<p><strong>component/main_calendar.dart</strong></p>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>isTodayHighlighted: false</td>
<td>오늘 날짜 강조를 없앰</td>
<td></td>
</tr>
</tbody></table>
<p><strong>component/schedule_bottom_sheet.dart</strong></p>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>createState()</td>
<td>State 객체를 생성하여 반환</td>
<td></td>
</tr>
<tr>
<td>final GlobalKey<FormState> formKey = GlobalKey()</td>
<td>폼을 조종하는 리모컨</td>
<td>폼에 넣은 것들을 관리</td>
</tr>
<tr>
<td>MediaQuery</td>
<td>SafeArea에 화면의 반 차지하는 컨테이너 위젯을 배치</td>
<td>폼에 넣은 것들을 관리</td>
</tr>
</tbody></table>
<h3 id="코드-동작-순서">코드 동작 순서</h3>
<blockquote>
<p>앱 시작 : *<em>main.dart *</em>
  LocalDatabase() 생성
GetIt에 DB 등록
HomeScreen 실행</p>
</blockquote>
<p>화면 생성 : <strong>home_screen.dart</strong>
  selectedDate = 오늘 날짜로 초기화
MainCalendar 표시
TodayBanner 표시
StreamBuilder로 DB 일정 목록 실시간 감시</p>
<blockquote>
<p>  날짜 선택 : <strong>MainCalendar</strong>
  → onDaySelected 실행
→ setState로 selectedDate 업데이트
→ TodayBanner 날짜 바뀜
→ StreamBuilder가 새 날짜 일정 자동으로 불러옴</p>
<p>  <strong>일정 추가 버튼 클릭</strong>
  FloatingActionButton 클릭
→ ScheduleBottomSheet 올라옴
→ CustomTextField로 시작시간/종료시간/내용 입력
→ 저장 버튼 클릭
→ 유효성 검사
→ 통과하면 LocalDatabase().createSchedule()로 DB 저장
→ BottomSheet 닫힘
→ StreamBuilder가 변화 감지해서 ScheduleCard 자동으로 목록에 추가</p>
<p>**  일정 목록 표시**
  StreamBuilder가 DB 변화 감지
→ ListView.builder로 ScheduleCard 목록 렌더링</p>
</blockquote>
<h3 id="코드-직접-변경-후-적용">코드 직접 변경 후 적용</h3>
<p>  <strong>schedule_bottom_sheet.dart</strong></p>
<pre><code class="language-dart">SizedBox(
                  height: 120,
                  child: CustomTextField(
                    label: &#39;내용&#39;,
                    isTime: false,
                    onSaved: (String? val) {
                      content = val;
                    },
                    validator: contentValidator,
                  ),
                ),</code></pre>
<blockquote>
<p>  저장 버튼을 보이게 하기 위해 height를 50으로 줄여봄
      *<em>height가 200일때 *</em>
  <img src="https://velog.velcdn.com/images/ttt0_0/post/ce71dd32-ada4-4561-8949-289e139b937e/image.png" alt=""></p>
</blockquote>
<p> ** height가 50일 때**
<img src="https://velog.velcdn.com/images/ttt0_0/post/1e1efd16-1658-4b58-a3ae-97e24b0660b4/image.png" alt=""></p>
<p>  =&gt; 내용 입력칸 사이즈가 작아짐</p>
<h3 id="에러---해결-방법">에러  &amp; 해결 방법</h3>
<blockquote>
<p><strong>🚨에러</strong> : 코드를 수정하고 실행해서 수정한 부분을 확인할 때 iphone 기기로 선택해서 실행했을 때 적용이 되지 않음. (mac으로 실행했을 때는 수정한 부분이 잘 적용됌)</p>
</blockquote>
<p>🔍 : <strong>빌드 캐시 문제</strong>였음 (이전 컴파일 결과물을 불러 재사용함)</p>
<blockquote>
</blockquote>
<p>✅해결방법 : 터미널에 flutter clean -&gt; flutter pub get -&gt; flutter run -&gt; iphone 선택
    <strong>이후</strong>에는 실행할 때 실행 버튼 누른 후 실행 중인 터미널에 <strong>r키</strong>를 눌러 수정 =&gt; Hot reload(UI가 조금 수정)</p>
<h3 id="새로-알게된-것">새로 알게된 것</h3>
<p>빌드 캐시 : flutter가 빌드(실행)할 때 매번 처음부터 컴파일하면 오래 걸려서, 이전에 컴파일한 결과물을 저장해두고 재사용한 것</p>
<h3 id="결과물">결과물</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/9aef46bc-415c-4199-a40a-60ac65312e96/image.png" alt="">
<img src="https://velog.velcdn.com/images/ttt0_0/post/1b5935fe-b930-483b-a075-8d04a3c804a9/image.png" alt="">
<img src="https://velog.velcdn.com/images/ttt0_0/post/6de99d68-6d9c-40c5-b77c-8f95fc2a39e5/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter 8일차] 일정 등록 기능과 DB 연동]]></title>
            <link>https://velog.io/@ttt0_0/Flutter-8%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@ttt0_0/Flutter-8%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 11 May 2026 14:54:50 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘의-목표--일정-등록-기능-추가와-flutter에서-db연동하기">오늘의 목표 : 일정 등록 기능 추가와 Flutter에서 DB연동하기</h2>
<h2 id="핵심-개념">핵심 개념</h2>
<blockquote>
<p>Stream을 사용하면 DB값이 바뀌면 화면도 자동 갱신된다
Drift는 Flutter에서 SQLite를 쉽게 관리하는 라이브러리이다.
LazyDatabase를 사용하면 DB를 필요할때만 열어서 성능이 최적화된다.</p>
</blockquote>
<h3 id="수업-코드">수업 코드</h3>
<p><strong>/component/schedult_bottom_sheet.dart</strong></p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:calendar_scheduler/const/colors.dart&#39;;
import &#39;package:calendar_scheduler/component/custom_text_field.dart&#39;;

class ScheduleBottomSheet extends StatefulWidget{
  const ScheduleBottomSheet({Key? key}) : super(key:key);

  @override
  State&lt;ScheduleBottomSheet&gt; createState() =&gt; _ScheduleBottomSheetState();
}

class _ScheduleBottomSheetState extends State&lt;ScheduleBottomSheet&gt; {
  @override
  Widget build(BuildContext context) {
    //키보드 높이 가져오기
    final bottomInset = MediaQuery.of(context).viewInsets.bottom;
    //viewInsets : 시스템이 차지하는 화면의 bottom: 아랫부분 크기를 알수 있음

    return SafeArea(
        child: Container(
          // MediaQuery : SafeArea에 화면의 반 차지하는 컨테이너 위젯을 배치
          height: MediaQuery.of(context).size.height/2 + bottomInset,
          color: Colors.white,
          child: Padding(
              padding: EdgeInsets.only(left:8, right:8, top:8),
            child: Column(
              children: [
                Row(
                  children: [
                    Expanded(
                      child: CustomTextField(
                        label: &#39;시작시간&#39;,
                        isTime: true,
                      ),
                    ),
                    const SizedBox(width: 16),
                    Expanded(
                      child: CustomTextField(
                        label: &#39;종료시간&#39;,
                        isTime: true,
                      ),
                    ),
                    const SizedBox(width: 16),
                  ],
                ),
                SizedBox(height: 8),
                Expanded(
                  child: CustomTextField(
                      label: &#39;내용&#39;,
                      isTime: false
                  ),
                ),
              ],
            ),
          ),
        ),
    );
  }
}</code></pre>
<p>=&gt; 사용자가 일정을 입력할 수 있도록 시작시간, 종료 시간, 내용을 입력받는 하단 창을 만드는 코드이다</p>
<p><strong>/database/drift_database.dart</strong></p>
<pre><code class="language-dart">import &#39;package:calendar_scheduler/model/schedule.dart&#39;;
import &#39;package:drift/drift.dart&#39;;

import &#39;package:drift/native.dart&#39;;
import &#39;package:path_provider/path_provider.dart&#39;;
import &#39;package:path/path.dart&#39; as p;
import &#39;dart:io&#39;;

part &#39;drift_database.g.dart&#39;;

@DriftDatabase(
  tables:[
    Schedules,
  ],
)

class LocalDatabase extends _$LocalDatabase {
  LocalDatabase():super(_openConnection());

  Stream&lt;List&lt;Schedule&gt;&gt; watchSechdules(DateTime date) =&gt;
      (select(schedules).. where((tbl) =&gt; tbl.date.equals(date))).watch();

  Future&lt;int&gt; createSchedule(SchedulesCompanion data) =&gt;
      into(schedules).insert(data);

  Future&lt;int&gt; removeSchedule(int id) =&gt;
      (delete(schedules)..where((tbl)=&gt;tbl.id.equals(id))).go();

  @override
  int get schemaVersion =&gt; 1;
}

LazyDatabase _openConnection(){
  return LazyDatabase(() async{
    final dbFolder = await getApplicationCacheDirectory();
    final file = File(p.join(dbFolder.path, &#39;db.sqlite&#39;));
    return NativeDatabase(file);
  });
}</code></pre>
<p>=&gt; Drift 데이터베이스로 일정 데이터를 저장, 조회, 삭제하고 DB 파일을 생성하는 코드이다.</p>
<p><strong>/model/schedule.dart</strong></p>
<pre><code class="language-dart">import &#39;package:drift/drift.dart&#39;;

class Schedules extends Table {
  IntColumn get id =&gt; integer().autoIncrement()();
  TextColumn get content =&gt; text()();
  DateTimeColumn get date =&gt; dateTime()();
  IntColumn get startTime =&gt; integer()();
  IntColumn get endTime =&gt; integer()();

}</code></pre>
<p>일정 테이블을 만드는 코드</p>
<h3 id="코드-분석">코드 분석</h3>
<p><strong>/component/schedult_bottom_sheet.dart</strong></p>
<pre><code class="language-dart">Widget build(BuildContext context) {
    //키보드 높이 가져오기
    final bottomInset = MediaQuery.of(context).viewInsets.bottom;
    //viewInsets : 시스템이 차지하는 화면의 bottom: 아랫부분 크기를 알수 있음

    return SafeArea(
        child: Container(
          // MediaQueㄴry : SafeArea에 화면의 반 차지하는 컨테이너 위젯을 배치
          height: MediaQuery.of(context).size.height/2 + bottomInset,
          color: Colors.white,
          child: Padding(
              padding: EdgeInsets.only(left:8, right:8, top:8),
            child: Column(
              children: [
                Row(
                  children: [
                    Expanded(
                      child: CustomTextField(
                        label: &#39;시작시간&#39;,
                        isTime: true,
                      ),
                    ),
                    const SizedBox(width: 16),
                    Expanded(
                      child: CustomTextField(
                        label: &#39;종료시간&#39;,
                        isTime: true,
                      ),
                    ),
                    const SizedBox(width: 16),
                  ],
                ),
                SizedBox(height: 8),
                Expanded(
                  child: CustomTextField(
                      label: &#39;내용&#39;,
                      isTime: false
                  ),
                ),
              ],
            ),
          ),
        ),
    );
  }</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>MediaQuery.of(context).viewInsets.bottom</td>
<td>키보드 높이 보기</td>
<td>화면에 아랫부분 크기를 알 수 있음</td>
</tr>
<tr>
<td>Expanded</td>
<td>입력창 크기 분배</td>
<td></td>
</tr>
<tr>
<td>SafeArea</td>
<td>안전 영역 확보</td>
<td></td>
</tr>
</tbody></table>
<p><strong>/database/drift_database.dart</strong></p>
<pre><code class="language-dart">class LocalDatabase extends _$LocalDatabase {
  LocalDatabase():super(_openConnection());

  Stream&lt;List&lt;Schedule&gt;&gt; watchSechdules(DateTime date) =&gt;
      (select(schedules).. where((tbl) =&gt; tbl.date.equals(date))).watch();

  Future&lt;int&gt; createSchedule(SchedulesCompanion data) =&gt;
      into(schedules).insert(data);

  Future&lt;int&gt; removeSchedule(int id) =&gt;
      (delete(schedules)..where((tbl)=&gt;tbl.id.equals(id))).go();

  @override
  int get schemaVersion =&gt; 1;
}

LazyDatabase _openConnection(){
  return LazyDatabase(() async{
    final dbFolder = await getApplicationCacheDirectory();
    final file = File(p.join(dbFolder.path, &#39;db.sqlite&#39;));
    return NativeDatabase(file);
  });
}</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>Stream</td>
<td>바뀐 DB값 화면에 적용</td>
</tr>
<tr>
<td>LazyDatabase</td>
<td>DB 필요할 때만 열어서 사용</td>
</tr>
<tr>
<td>Future</td>
<td>나중에 줄 결과값</td>
</tr>
<tr>
<td>schemaVersion</td>
<td>데이터베이스 구조의 버전을 관리하는 값</td>
</tr>
</tbody></table>
<p><strong>/model/schedule.dart</strong></p>
<pre><code class="language-dart">import &#39;package:drift/drift.dart&#39;;

class Schedules extends Table {
  IntColumn get id =&gt; integer().autoIncrement()();
  TextColumn get content =&gt; text()();
  DateTimeColumn get date =&gt; dateTime()();
  IntColumn get startTime =&gt; integer()();
  IntColumn get endTime =&gt; integer()();

}</code></pre>
<p>=&gt; DB테이블 설계, 생성</p>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>autoIncrement()</td>
<td>자동 증가</td>
</tr>
</tbody></table>
<h3 id="새로-알게된-것">새로 알게된 것</h3>
<p>Flutter에서 DB를 생성할 때의 SQL문법을 새로 알게 되었다</p>
<h3 id="결과물">결과물</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/306494f7-f128-42ef-a322-2422e69f12b9/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter 7일차] ]]></title>
            <link>https://velog.io/@ttt0_0/Flutter-7%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@ttt0_0/Flutter-7%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 22 Apr 2026 14:53:24 GMT</pubDate>
            <description><![CDATA[<h3 id="수업-주제--calendar_scheduler-프로젝트-일정-추가하기-기능-넣기">수업 주제 : calendar_scheduler 프로젝트 일정 추가하기 기능 넣기</h3>
<h3 id="수업-코드">수업 코드</h3>
<p><strong>/component/custom_text_field.dart</strong></p>
<pre><code class="language-dart">import &#39;package:calendar_scheduler/const/colors.dart&#39;;
import &#39;package:flutter/material.dart&#39;;
import &#39;package:flutter/services.dart&#39;;

class CustomTextField extends StatelessWidget {
  final String label;
  final bool isTime;

  const CustomTextField({
    required this.label,
    required this.isTime,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          label,
          style: TextStyle(
            color: PRIMARY_COLOR,
            fontWeight: FontWeight.w600,
          ),
        ),
        Expanded(
          flex: isTime ? 0 : 1,
          child: TextFormField(
            cursorColor: Colors.grey,
            maxLines: isTime ? 1 : null,
            expands: !isTime,
            keyboardType:
            isTime ? TextInputType.number : TextInputType.multiline,
            inputFormatters: isTime
                ? [FilteringTextInputFormatter.digitsOnly]
                : [],
            decoration: InputDecoration(
              border: InputBorder.none,
              filled: true,
              fillColor: Colors.grey[300],
              suffixText: isTime ? &#39;시&#39; : null,
            ),
          ),
        ),
      ],
    );
  }
}</code></pre>
<p><strong>/component/main_calendar.dart</strong></p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:table_calendar/table_calendar.dart&#39;;
import &#39;package:calendar_scheduler/const/colors.dart&#39;;


class MainCalendar extends StatelessWidget{
  final OnDaySelected onDaySelected; //날짜 선택시 실행
  final DateTime selectedDate; //선택된 날짜

  const MainCalendar({
    required this.onDaySelected,
    required this.selectedDate,
  });

  @override
  Widget build(BuildContext context) {
    return TableCalendar(
      onDaySelected: onDaySelected,
      selectedDayPredicate: (date)=&gt; //선택 된 날짜 구분 로직
      date.year == selectedDate.year&amp;&amp;
      date.month == selectedDate.month&amp;&amp;
      date.day == selectedDate.day,

      firstDay: DateTime(1800, 1, 1), //첫째날
      lastDay : DateTime(3000, 1, 11), //마지막 날
      focusedDay: DateTime.now(), //화면에 보여지는 날

      headerStyle: HeaderStyle(
        titleCentered: true,
        formatButtonVisible: false,
        titleTextStyle: TextStyle(
          fontWeight: FontWeight.w700,
          fontSize: 16.0,
        ),

      ),
      calendarStyle:CalendarStyle(
        isTodayHighlighted: false,
        defaultDecoration: BoxDecoration(
          borderRadius: BorderRadius.circular(6.0),
          color:LIGHT_GREY_COLOR
        ),
          weekendDecoration: BoxDecoration(
              borderRadius: BorderRadius.circular(6.0),
              color:LIGHT_GREY_COLOR
          ),
          selectedDecoration: BoxDecoration(
              borderRadius: BorderRadius.circular(6.0),
              border: Border.all(
                color: PRIMARY_COLOR,
                width: 1.0,
              ),
          ),
        defaultTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: DARK_GREY_COLOR,
        ),
        weekendTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: DARK_GREY_COLOR,
        ),
        selectedTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: PRIMARY_COLOR,
        ),
      ),

    );
  }


}</code></pre>
<p><strong>/component/schedule_bottom_sheet.dart</strong></p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:calendar_scheduler/component/custom_text_field.dart&#39;;

class ScheduleBottomSheet extends StatefulWidget{
  const ScheduleBottomSheet({Key? key}) : super(key:key);

  @override
  State&lt;ScheduleBottomSheet&gt; createState() =&gt; _ScheduleBottomSheetState();

}

class _ScheduleBottomSheetState extends State&lt;ScheduleBottomSheet&gt; {
  @override
  Widget build(BuildContext context) {
    return SafeArea(
        child: Container(
          // MediaQuery : SafeArea에 화면의 반 차지하는 컨테이너 위젯을 배치
          height: MediaQuery.of(context).size.height/2,
          color: Colors.white,
          child: CustomTextField(label: &#39;시작시간&#39;, isTime: true,)
        ),
    );
  }
}</code></pre>
<p><strong>/component/schedule_card.dart</strong></p>
<pre><code class="language-dart">//schedule_card.dart

import &#39;package:calendar_scheduler/const/colors.dart&#39;;
import &#39;package:flutter/material.dart&#39;;

class ScheduleCard extends StatelessWidget{
  final int startTime;
  final int endTime;
  final String content;

  const ScheduleCard({
    required this.startTime,
    required this.endTime,
    required this.content,
    Key ? key,
  }) : super(key:key);

  @override
  Widget build(BuildContext context) {
    // 바깥 박스
    return Container(
      decoration: BoxDecoration(
        // 테두리
        border: Border.all(
          width: 1.0,
          color: PRIMARY_COLOR,
        ),
        // 테두리 둥글기정도
        borderRadius: BorderRadius.circular(8.0),
      ),
      child: Padding(
        // 텍스트가 테두리에 붙지 않게
        padding: const EdgeInsets.all(16),
        // Intrinsic : 본질적인
        // 최대 크기만큼 내부 높이를 최대로 맞춰줌
        child: IntrinsicHeight(
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              _Time(startTime: startTime, endTime: endTime,),
              SizedBox(width: 16,), // 여백
              _Content(content: content),
              SizedBox(width: 16,)
            ],
          ),
        )
      ),
    );
  }
}

//파일 내부에서만 사용하는 프라이빗 클래스 (_)
class _Time extends StatelessWidget{
  final int startTime; //값 재할당 불가 -&gt; final
  final int endTime; //값 재할당 불가 -&gt; final

  // 성능 최적화, 재할당 불가 -&gt;  const
  const _Time({
    required this.startTime,
    required this.endTime,
    Key? key,
  }) : super(key:key);

  @override
  Widget build(BuildContext context) {
    // 텍스트 스타일 정의
    final textStyle = TextStyle(
      fontWeight: FontWeight.w600, //약간 굵은 글씨
      color:PRIMARY_COLOR, //저번에 지정함
      fontSize: 16.0,
    );

    //세로방향 위젯 배치
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start, //왼쪽 정렬
      children: [
        Text(
          &#39;${startTime.toString().padLeft(2, &#39;0&#39;)}:00&#39;,
          style: textStyle.copyWith(
            fontSize: 10.0
          ),
        ),
        Text(
          &#39;${endTime.toString().padLeft(2, &#39;0&#39;)}:00&#39;,
          style: textStyle.copyWith(
            fontSize: 10.0,
          ),
        ),
      ],
    );
  }
}

class _Content extends StatelessWidget {
  final String content;

  const _Content({
    required this.content,
    Key ? key,
  }) : super(key : key);

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Text(
        content,
      ),
    );
  }
}

</code></pre>
<p><strong>/component/today_banner.dart</strong></p>
<pre><code class="language-dart">import &#39;package:calendar_scheduler/const/colors.dart&#39;;
import &#39;package:flutter/material.dart&#39;;

class TodayBanner extends StatelessWidget {
  final DateTime selectedDate;
  final int count;

  const TodayBanner({
    required this.selectedDate,
    required this.count,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final textStyle = TextStyle(
      fontWeight: FontWeight.w600,
      color: Colors.white,
    );

    return Container(
      color: PRIMARY_COLOR,
      child: Padding(
        padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              &#39;${selectedDate.year}년 ${selectedDate.month}월 ${selectedDate.day}일&#39;,
              style: textStyle,
            ),
            Text(
              &#39;$count개&#39;,
              style: textStyle,
            ),
          ],
        ),
      ),
    );
  }
}</code></pre>
<p><strong>/const/colors.dart</strong></p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;

const PRIMARY_COLOR = Color(0xFFFE3977E9);
final LIGHT_GREY_COLOR = Colors.grey[200]!;
final DARK_GREY_COLOR = Colors.grey[600]!;
final TEXTFIELD_FILL_COLOR = Colors.grey[300]!;</code></pre>
<p><strong>/screen/home_screen.dart</strong></p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:calendar_scheduler/component/main_calendar.dart&#39;;
import &#39;package:calendar_scheduler/component/schedule_card.dart&#39;;
import &#39;package:calendar_scheduler/component/today_banner.dart&#39;;
import &#39;package:calendar_scheduler/component/schedule_bottom_sheet.dart&#39;;
import &#39;package:calendar_scheduler/const/colors.dart&#39;;


class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  State&lt;HomeScreen&gt; createState() =&gt; _HomeScreenState();
}

class _HomeScreenState extends State&lt;HomeScreen&gt;{
  DateTime selectedDate = DateTime.utc(

    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        backgroundColor: PRIMARY_COLOR,
        onPressed: (){
          showModalBottomSheet(
              context: context,
              isDismissible : true,
              builder: (_) =&gt;ScheduleBottomSheet(),
          );
        },
        child: Icon(
          Icons.add,
        ),
      ),

      body: SafeArea(
          child: Column(
            children: [
              MainCalendar(
                selectedDate: selectedDate,
                //선택된 날짜 전달 코드

                onDaySelected: onDaySelected,
              ),
              SizedBox(height:8),
              TodayBanner(
                  selectedDate: selectedDate,
                  count: 0
              ),
              SizedBox(height:8),
              ScheduleCard(startTime: 12,
                  endTime: 14,
                  content: &#39;프로그래밍 공부&#39;
              ),
            ],
          )
      ),
    );
  }

  void onDaySelected(DateTime selectedDate, DateTime focusedDate) {
    // 날짜 선택될 때마다 실행할 함수
    setState(() {
      this.selectedDate = selectedDate;
    });

  }
}</code></pre>
<p><strong>main.dart</strong></p>
<pre><code class="language-dart">import &#39;package:calendar_scheduler/screen/home_screen.dart&#39;;
import &#39;package:flutter/material.dart&#39;;


void main() {
  runApp(
    MaterialApp(
      home: HomeScreen()
    ),
  );
}

</code></pre>
<h3 id="코드-분석">코드 분석</h3>
<p><strong>/component/custom_text_field.dart</strong></p>
<pre><code class="language-dart">Expanded(
          flex: isTime ? 0 : 1,
          child: TextFormField(
            cursorColor: Colors.grey,
            // isTime = true : 한 줄 입력
            maxLines: isTime ? 1 : null,
            // isTime = false : 여러 줄 입력 (박스 형태로 늘어남)
            expands: !isTime,
            keyboardType:
            isTime ? TextInputType.number : TextInputType.multiline,
            // TextInputType.number : 숫자 키보드
            // TextInputType.multiline : 일반 키보드
            inputFormatters: isTime
                ? [FilteringTextInputFormatter.digitsOnly] // 숫자만 입력가능하게 함
                : [],
            decoration: InputDecoration(
              border: InputBorder.none,
              filled: true,
              fillColor: Colors.grey[300],
              //
              suffixText: isTime ? &#39;시&#39; : null,
            ),
          ),
        ),</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>final bool isTime</td>
<td>입력창 종류 결정</td>
<td>true : 한 줄 입력, false : 여러 줄 입력</td>
</tr>
<tr>
<td>keyboardType</td>
<td>키보드 종류 결정</td>
<td>TextInputType.number : 숫자 키보드, TextInputType.multiline : 일반키보드</td>
</tr>
<tr>
<td>FilteringTextInputFormatter.digitsOnly</td>
<td>입력창에 숫자만 들어오게 막는 필터 역할</td>
<td></td>
</tr>
<tr>
<td>suffixText: isTime ? &#39;시&#39; : null</td>
<td>입력창 오른쪽 끝에 붙는 텍스트</td>
<td>isTime이 true -&gt; 입력받은 텍스트 오른쪽에 &#39;시&#39;붙임, false -&gt; null</td>
</tr>
</tbody></table>
<p><strong>/component/schedule_bottom_sheet.dart</strong></p>
<pre><code class="language-dart">return SafeArea(
        child: Container(
          // MediaQuery : SafeArea에 화면의 반 차지하는 컨테이너 위젯을 배치
          height: MediaQuery.of(context).size.height/2,
          color: Colors.white,
          child: CustomTextField(label: &#39;시작시간&#39;, isTime: true,)
        ),
    );</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>SafeArea</td>
<td>폰 화면에서 잘리는 부분을 피해서 배치해주는 위젯</td>
</tr>
<tr>
<td>MediaQuery</td>
<td>화면의 크기를 알려줌</td>
</tr>
<tr>
<td>MediaQuery.of(context).size.height/2</td>
<td>화면의 반을 차지하는 컨테이너 위젯</td>
</tr>
</tbody></table>
<p><strong>/component/schedule_card.dart</strong></p>
<pre><code class="language-dart">Widget build(BuildContext context) {
    // 바깥 박스
    return Container(
      decoration: BoxDecoration(
        // 테두리
        border: Border.all(
          width: 1.0,
          color: PRIMARY_COLOR,
        ),
        // 테두리 둥글기정도
        borderRadius: BorderRadius.circular(8.0),
      ),
      child: Padding(
        // 텍스트가 테두리에 붙지 않게
        padding: const EdgeInsets.all(16),
        // Intrinsic : 본질적인
        // 최대 크기만큼 내부 높이를 최대로 맞춰줌
        child: IntrinsicHeight(
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              _Time(startTime: startTime, endTime: endTime,),
              SizedBox(width: 16,), // 여백
              _Content(content: content),
              SizedBox(width: 16,)
            ],
          ),
        )
      ),
    );

//세로방향 위젯 배치
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start, //왼쪽 정렬
      children: [
        Text(
          &#39;${startTime.toString().padLeft(2, &#39;0&#39;)}:00&#39;,
          style: textStyle.copyWith(
            fontSize: 10.0
          ),
        ),
        Text(
          &#39;${endTime.toString().padLeft(2, &#39;0&#39;)}:00&#39;,
          style: textStyle.copyWith(
            fontSize: 10.0,
          ),
        ),
      ],
    );</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>Container</td>
<td>바깥 테두리 박스</td>
<td></td>
</tr>
<tr>
<td>BorderRadius.circular()</td>
<td>테두리 둥글기 정도</td>
<td></td>
</tr>
<tr>
<td>padding: EdgeInsets.all(16)</td>
<td>테두리 붙지않게 만듦</td>
<td></td>
</tr>
<tr>
<td>IntrinsicHeight()</td>
<td>내부 요소 높이를 가장 큰 기준에 맞춰줌</td>
<td></td>
</tr>
<tr>
<td>Row(crossAxisAlignment: CrossAxisAlignment.stretch,)</td>
<td>가로 배치</td>
<td>Row : 가로 (행) -&gt; crossAxisAlignment : 세로 (열)</td>
</tr>
<tr>
<td>startTime.toString().padLeft(2, &#39;0&#39;)</td>
<td>한자리 숫자 코드 -&gt; 왼쪽에 &#39;0&#39;붙여 두자리 만듦</td>
<td></td>
</tr>
</tbody></table>
<p><strong>/component/today_banner.dart</strong></p>
<pre><code class="language-dart">return Container(
      color: PRIMARY_COLOR,
      child: Padding(
        padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              &#39;${selectedDate.year}년 ${selectedDate.month}월 ${selectedDate.day}일&#39;,
              style: textStyle,
            ),
            Text(
              &#39;$count개&#39;,
              style: textStyle,
            ),
          ],
        ),
      ),
    );</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>Padding(padding: EdgeInsets.symmetric(horizontal: 16,vertical: 8),)</td>
<td>안쪽 여백</td>
<td>좌우 : 16, 상하 : 8</td>
</tr>
<tr>
<td>Row(mainAxisAlignment: MainAxisAlignment.spaceBetween,)</td>
<td>가로 배치, 양끝 정렬</td>
<td></td>
</tr>
</tbody></table>
<h3 id="새로-알게된-것">새로 알게된 것</h3>
<blockquote>
<p>오늘 나간 진도중에서 기본 틀 코드빼고는 거의다 새로 알게되었다. 
keyboardType : isTime의 값에 따라 키보드 형태 결정
FilteringTextInputFormatter.digitsOnly : 숫자만 거르는 필터 역할
MediaQuery : 화면의 사이즈
IntrinsicHeight() : 내부 요소 높이를 최대로 맞춰줌
padding: EdgeInsets.all(16) : 안쪽 여백 맞추기
++ MainAxisAlignment, CrossAxisAlignment는 완벽 이해된 것 같다.</p>
</blockquote>
<h3 id="결과물">결과물</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/21307ab8-43f5-4a5b-a8c6-41b16c05d584/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter 6일차] ]]></title>
            <link>https://velog.io/@ttt0_0/Flutter-6%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@ttt0_0/Flutter-6%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Tue, 21 Apr 2026 06:50:39 GMT</pubDate>
            <description><![CDATA[<h3 id="수업-주제--table_calendar-플러그인-활용">수업 주제 : table_calendar 플러그인 활용</h3>
<h3 id="전체-코드">전체 코드</h3>
<p><strong>/screen/main.dart</strong></p>
<pre><code class="language-dart">import &#39;package:calendar_scheduler/screen/home_screen.dart&#39;;
import &#39;package:flutter/material.dart&#39;;


void main() {
  runApp(
    MaterialApp(
      home: HomeScreen()
    ),
  );
}

</code></pre>
<p><strong>/screen/home_screen.dart</strong></p>
<pre><code>import &#39;package:flutter/material.dart&#39;;
import &#39;package:calendar_scheduler/component/main_calendar.dart&#39;;


class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  State&lt;HomeScreen&gt; createState() =&gt; _HomeScreenState();
}

class _HomeScreenState extends State&lt;HomeScreen&gt;{
  DateTime selectedDate = DateTime.utc(
    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
          child: Column(
            children: [
              MainCalendar(
                selectedDate: selectedDate,
                //선택된 날짜 전달 코드

                onDaySelected: onDaySelected,
              ),
            ],
          )
      ),
    );
  }

  void onDaySelected(DateTime selectedDate, DateTime focusedDate) {
    // 날짜 선택될 때마다 실행할 함수
    setState(() {
      this.selectedDate = selectedDate;
    });

  }
}</code></pre><p><strong>/component/main_calendar.dart</strong></p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:table_calendar/table_calendar.dart&#39;;
import &#39;package:calendar_scheduler/const/colors.dart&#39;;


class MainCalendar extends StatelessWidget{
  final OnDaySelected onDaySelected; //날짜 선택시 실행
  final DateTime selectedDate; //선택된 날짜

  const MainCalendar({
    required this.onDaySelected,
    required this.selectedDate,
  });

  @override
  Widget build(BuildContext context) {
    return TableCalendar(
      onDaySelected: onDaySelected,
      selectedDayPredicate: (date)=&gt; //선택 된 날짜 구분 로직
      date.year == selectedDate.year&amp;&amp;
      date.month == selectedDate.month&amp;&amp;
      date.day == selectedDate.day,

      firstDay: DateTime(1800, 1, 1), //첫째날
      lastDay : DateTime(3000, 1, 11), //마지막 날
      focusedDay: DateTime.now(), //화면에 보여지는 날

      headerStyle: HeaderStyle(
        titleCentered: true,
        formatButtonVisible: false,
        titleTextStyle: TextStyle(
          fontWeight: FontWeight.w700,
          fontSize: 16.0,
        ),

      ),
      calendarStyle:CalendarStyle(
        isTodayHighlighted: false,
        defaultDecoration: BoxDecoration(
          borderRadius: BorderRadius.circular(6.0),
          color:LIGHT_GREY_COLOR
        ),
          weekendDecoration: BoxDecoration(
              borderRadius: BorderRadius.circular(6.0),
              color:LIGHT_GREY_COLOR
          ),
          selectedDecoration: BoxDecoration(
              borderRadius: BorderRadius.circular(6.0),
              border: Border.all(
                color: PRIMARY_COLOR,
                width: 1.0,
              ),
          ),
        defaultTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: DARK_GREY_COLOR,
        ),
        weekendTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: DARK_GREY_COLOR,
        ),
        selectedTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: PRIMARY_COLOR,
        ),
      ),

    );
  }


}</code></pre>
<p><strong>/const/color.dart</strong></p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;

const PRIMARY_COLOR = Color(OxFFFE3977E9);
final LIGHT_GREY_COLOR = Colors.grey[200]!;
final DARK_GREY_COLOR = Colors.grey[600]!;
final TEXTFIELD_FILL_COLOR = Colors.grey[300]!;</code></pre>
<h3 id="코드-분석">코드 분석</h3>
<h4 id="home_screendart">home_screen.dart</h4>
<pre><code class="language-dart">DateTime selectedDate = DateTime.utc(
    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );

 void onDaySelected(DateTime selectedDate, DateTime focusedDate) {
    // 날짜 선택될 때마다 실행할 함수
    setState(() {
      this.selectedDate = selectedDate;
    });</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>selectedDate</td>
<td>오늘 날짜를 기본값으로 설정</td>
<td></td>
</tr>
<tr>
<td>utc</td>
<td>시간 영향을 제거, 날짜만 비교하기 위해 사용</td>
<td>전 세계가 기준으로 사용하는 표준 시간</td>
</tr>
<tr>
<td>onDaySelected</td>
<td>날짜 클릭될 때마다 실행</td>
<td></td>
</tr>
</tbody></table>
<h4 id="main_calendardart">main_calendar.dart</h4>
<pre><code class="language-dart">const MainCalendar({
    required this.onDaySelected,
    required this.selectedDate,
  });

  return TableCalendar(
      onDaySelected: onDaySelected,
      selectedDayPredicate: (date)=&gt; //선택 된 날짜 구분 로직
      date.year == selectedDate.year&amp;&amp;
      date.month == selectedDate.month&amp;&amp;
      date.day == selectedDate.day,

      firstDay: DateTime(1800, 1, 1), //첫째날
      lastDay : DateTime(3000, 1, 11), //마지막 날
      focusedDay: DateTime.now(), //화면에 보여지는 날

      headerStyle: HeaderStyle(
        titleCentered: true,
        formatButtonVisible: false,
        titleTextStyle: TextStyle(
          fontWeight: FontWeight.w700,
          fontSize: 16.0,
        ),

      ),
      calendarStyle:CalendarStyle(
        isTodayHighlighted: false,
        defaultDecoration: BoxDecoration(
          borderRadius: BorderRadius.circular(6.0),
          color:LIGHT_GREY_COLOR
        ),
          weekendDecoration: BoxDecoration(
              borderRadius: BorderRadius.circular(6.0),
              color:LIGHT_GREY_COLOR
          ),
          selectedDecoration: BoxDecoration(
              borderRadius: BorderRadius.circular(6.0),
              border: Border.all(
                color: PRIMARY_COLOR,
                width: 1.0,
              ),
          ),
        defaultTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: DARK_GREY_COLOR,
        ),
        weekendTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: DARK_GREY_COLOR,
        ),
        selectedTextStyle: TextStyle(
          fontWeight: FontWeight.w600,
          color: PRIMARY_COLOR,
        ),
      ),

    );

</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>MainCalendar</td>
<td>캘린더 UI 컴포넌트</td>
<td>재사용 가능</td>
</tr>
<tr>
<td>TableCalendar</td>
<td>캘린더 위젯</td>
<td>외부 패키지</td>
</tr>
<tr>
<td>onDaySelected</td>
<td>날짜 클릭될 때마다 실행</td>
<td>부모에서 전달받는 콜백함수</td>
</tr>
<tr>
<td>selectedDayPredicate</td>
<td>선택된 날짜인지 구분하는 로직</td>
<td>날짜 비교 로직</td>
</tr>
<tr>
<td>calendarStyle</td>
<td>폰트 사이즈, 컬러 변경</td>
<td>캘린더 디자인 설정</td>
</tr>
</tbody></table>
<h4 id="colordart">color.dart</h4>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;

const PRIMARY_COLOR = Color(OxFFFE3977E9);
final LIGHT_GREY_COLOR = Colors.grey[200]!;
final DARK_GREY_COLOR = Colors.grey[600]!;
final TEXTFIELD_FILL_COLOR = Colors.grey[300]!;</code></pre>
<table>
<thead>
<tr>
<th>코드</th>
<th>역할</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>PRIMARY_COLOR</td>
<td>앱의 대표 색상</td>
<td>주요 강조 색</td>
</tr>
<tr>
<td>LIGHT_GREY_COLOR</td>
<td>밝은 회색 배경 색</td>
<td>기본 UI 배경</td>
</tr>
<tr>
<td>DARK_GREY_COLOR</td>
<td>어두운 회색 텍스트 색</td>
<td>가독성</td>
</tr>
<tr>
<td>TEXTFIELD_FILL_COLOR</td>
<td>입력창 배경 색</td>
<td>TextField 등에 사용</td>
</tr>
<tr>
<td>### 새로 알게된 것</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<blockquote>
<p>table_calendar 플러그인을 처음 사용해보았다.
쪽지시험 공부를 하면서 콜백함수가 이해가 가지 않았는데 이번 프로젝트에서 한번 더 사용하면서 콜백함수를 어떤 상황에서 사용할 수 있는지 알게되었다.
콜백함수는 부모가 함수를 전달하고 자식에서 예를 들어 클릭이 발생했을 때 부모 함수를 실행하는 것이라고 이해했다.
코드를 보면서 생각보다 짧은 코드로 달력을 구현했다는 것이 신기했다.</p>
</blockquote>
<blockquote>
<ul>
<li>TableCalendar를 사용하여 간단하게 캘린더 UI 구현 가능</li>
</ul>
</blockquote>
<blockquote>
<ul>
<li>DateTime.utc : 시간대 영향을 제거하고 날짜만 정확하게 비교하기 위해 사용</li>
</ul>
</blockquote>
<h3 id="헷갈리는-부분">헷갈리는 부분</h3>
<p>언제 어떤 함수를 사용해야 적절할지가 헷갈린다.
그리고 플러터에서 들여쓰기할 때 쉼표를 쓰기도 하고 세미콜론으로 마치는 경우도 있는데 그것이 헷갈린다</p>
<h3 id="결과물">결과물</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/55576f10-ca93-45f3-a0a5-ff3a29697ccb/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter 5일차] ]]></title>
            <link>https://velog.io/@ttt0_0/Flutter-5%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@ttt0_0/Flutter-5%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Tue, 14 Apr 2026 08:25:17 GMT</pubDate>
            <description><![CDATA[<h2 id="flutter-복습">Flutter 복습</h2>
<blockquote>
<h4 id="statelesswidget"><strong>StatelessWidget</strong></h4>
<p>상태가 변하지 않음
build() : 화면 그리는 함수</p>
</blockquote>
<blockquote>
<h4 id="statefulwidget"><strong>StatefulWidget</strong></h4>
<p>상태가 변하는 위젯
-&gt; 사용자의 조작, 시간에 따라 화면이 변해야함</p>
</blockquote>
<h4 id="homescreen껍데기">HomeScreen(껍데기)</h4>
<h4 id="_homescreenstate실제-내용">_HomeScreenState(실제 내용)</h4>
<p>-&gt; 클래스를 2개로 나눠 
위젯을 효율적으로 관리함</p>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/b81af79e-101d-4388-9e10-61b4062698d0/image.png" alt=""></p>
<h4 id="앱-실행-흐름">앱 실행 흐름</h4>
<ol>
<li><strong>main()</strong> : 시작점</li>
<li><strong>runApp()</strong> : 실행 함수</li>
<li>*<em>MaterialApp *</em>: 앱 전체 감싸는 위젯</li>
<li><strong>HomeScreen()</strong> : 첫 화면 지정</li>
</ol>
<p>레이아웃 위젯 - 화면 배치</p>
<h4 id="1-column--세로-배치-위---아래">1. Column : 세로 배치 (위 -&gt; 아래)</h4>
<pre><code>children : [] 여러 위젯을 받음</code></pre><h4 id="2-row--가로-배치-왼쪽---오른쪽">2. Row : 가로 배치 (왼쪽 -&gt; 오른쪽)</h4>
<h4 id="mainaxis-주축">mainAxis (주축)</h4>
<ul>
<li>start : 위로 몰아 배치</li>
<li>center : 가운데로 모아 배치</li>
<li>spaceBetween : 양끝에 붙이고 나머지 균등 분배</li>
<li>spaceEvenly : 모든 간격을 균등하게</li>
<li>spaceAround : 양쪽 끝 간격은 절반, 나머지 균등</li>
</ul>
<h4 id="crossaxis-교차축">crossAxis (교차축)</h4>
<h4 id="sizedbox--여백-생성">SizedBox : 여백 생성</h4>
<p>(=&gt; CSS의 padding, margin)</p>
<h4 id="safearea--안전-영역-확보빈-공간">SafeArea : 안전 영역 확보(빈 공간)</h4>
<p>ex) 노치(카메라 영역), 홈 인디케이터를 피해서 영역을 자동으로 피해서 내용을 배치</p>
<p>이미지 넣는 순서</p>
<ol>
<li>폴더 만들기<ul>
<li>asset/image/ 폴더 생성 후 이미지 파일을 저장</li>
</ul>
</li>
<li>pubspec.yaml 등록<pre><code>flutter : 
 assets :
     - asset/img/</code></pre>asset/img/ 폴더 안의 모든 파일 사용 가능
저장 후 <strong>Pub get</strong></li>
<li>코드에서 사용<pre><code>Image.asset(&#39;asset/img/image.jpg&#39;)</code></pre></li>
</ol>
<p>스타일 한 곳에서 관리
Theme.of(context).textTheme;
=&gt; context : 위치 알려줌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter 4일차] / U_and_I]]></title>
            <link>https://velog.io/@ttt0_0/Flutter-4%EC%9D%BC%EC%B0%A8-UandI</link>
            <guid>https://velog.io/@ttt0_0/Flutter-4%EC%9D%BC%EC%B0%A8-UandI</guid>
            <pubDate>Thu, 09 Apr 2026 01:39:34 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ttt0_0/post/df4e1b9c-1a54-46e6-91dd-a78dac44548e/image.png" alt=""></p>
<blockquote>
<h3 id="datetime">DateTime</h3>
<p>: 날짜와 시간을 다루는 클래스
속성 : year, month, day, hour, minute, second...</p>
</blockquote>
<blockquote>
<p><strong>D-Day()</strong> 
difference() : 두 날짜 차이 계산
inDays : 일 단위로 변환</p>
</blockquote>
<blockquote>
<h3 id="정렬-옵션">정렬 옵션</h3>
</blockquote>
<ul>
<li><strong>bottomCenter</strong> : 아래 가운데</li>
<li><strong>bottomLeft</strong> : 아래 왼쪽
..</li>
</ul>
<blockquote>
<p>날짜 선택 시 <strong>firstDay</strong> 변경
onDateTimeChanged: (date) {
  setState(() {
    firstDay = date;
  });
}
firstDay = firstDay.subtract(Duration(days: 1));</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter 3일차] ]]></title>
            <link>https://velog.io/@ttt0_0/Flutter-3%EC%9D%BC%EC%B0%A8-iaccfid0</link>
            <guid>https://velog.io/@ttt0_0/Flutter-3%EC%9D%BC%EC%B0%A8-iaccfid0</guid>
            <pubDate>Mon, 06 Apr 2026 11:43:10 GMT</pubDate>
            <description><![CDATA[<h3 id="flutter-생명주기">Flutter 생명주기</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/dfd6d0c6-30dd-4453-88bd-a6d07c2edaaf/image.jpg" alt=""></p>
<blockquote>
<p>createState() → initState() → build()
<strong>createState()</strong>
State 객체 생성
아직 UI 없음
<strong>initState()</strong>
초기 1회 실행
API 호출, 변수 초기화, listener 등록
<strong>build()</strong>
UI를 그리는 함수
여러 번 호출됨 → 반드시 가볍게 유지해야 함
setState() → build()
버튼 클릭, 스와이프 등 이벤트 발생
setState() 호출 시 상태 변경 + rebuild 요청
dispose()
화면이 사라질 때 딱 한번 실행, 메모리 정리 필수
controller 해제, stream 종료, listener 제거</p>
</blockquote>
<blockquote>
<h4 id="비동기처리">비동기처리</h4>
<p>onPressed: () async {
  await Future.delayed(Duration(seconds: 2));
  print(&quot;완료&quot;);
}
작업이 끝날 때까지 기다린 후 다음 코드 실행
<strong>showCupertinoDialog()</strong>
iOS 스타일로 대화창을 띄우는 함수</p>
</blockquote>
<blockquote>
<p><strong>BuildContext</strong>
위젯의 위치 정보
위젯 트리에서 현재 위치를 나타냄</p>
</blockquote>
<blockquote>
<h4 id="statelesswidget">StatelessWidget</h4>
<p>한 번 만들어지면 절대 변하지 않음
UI가 고정
텍스트, 아이콘, 고정된 화면
데이터 변경이 없는 경우</p>
</blockquote>
<h4 id="statefulwidget">StatefulWidget</h4>
<p>값이 바뀌면 UI도 바뀜
내부에 State 객체 존재
setState() 사용 가능
버튼 클릭
입력값 변화
API 데이터 반영
애니메이션 효과</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter 3일차] / image_carousel]]></title>
            <link>https://velog.io/@ttt0_0/Flutter-3%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@ttt0_0/Flutter-3%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 01 Apr 2026 13:06:04 GMT</pubDate>
            <description><![CDATA[<h3 id="위젯-생명주기">위젯 생명주기</h3>
<ol>
<li><p>StatelessWidget</p>
<ul>
<li>상태 없음</li>
<li>다시 그리려면 전체 새로 생성</li>
<li><blockquote>
<p>고정UI, 아이콘</p>
</blockquote>
</li>
</ul>
</li>
<li><p>StatefulWidget</p>
<ul>
<li>상태 있음</li>
<li>값이 바뀌면 화면 자동 변경</li>
<li><blockquote>
<p>버튼 클릭, API 데이터, 웹뷰 로딩, 입력값</p>
</blockquote>
</li>
</ul>
</li>
</ol>
<p>Pubspec.yaml -&gt; 의존성 있는 것들 저장하는 파일</p>
<h4 id="key">Key</h4>
<p>: 위젯을 구분하기위한 아이디
=&gt; list, update할 떄 필요</p>
<h4 id="buildcontext">BuildContext</h4>
<p>: 위젯의 위젯 정보, 위치 알려줌</p>
<h4 id="scaffold">Scaffold</h4>
<p>: 기본 뼈대</p>
<h4 id="pageview">PageView</h4>
<p>➡️ 여러개의 위젯을 단독 페이지로 생성하고 가로 또는 세로 스와이프로 페이지를 넘길 수 있게 하는 위젯</p>
<blockquote>
<p>fit:BoxFit.<strong>contain</strong> -&gt; 이미지가 잘리지 않는 선에서 최대한 크게 늘리기
fit:BoxFit.<strong>cover</strong> -&gt; 부모 위젯 전체를 덮는 선에서 최소한 크기로 조절
fit:BoxFit.<strong>fill</strong>-&gt; 이미지의 비율을 무시하고 부모 위젯의 이미지 비율대로 크기를 조절
fit:BoxFit.<strong>fitHeight</strong>-&gt; 이미지의 비율을 유지한 채로 부모 위젯의 높이에 이미지의 높이를 맞춤
fit:BoxFit.<strong>fitWidth</strong>-&gt; 이미지의 비율을 유지한 채로 부모 위젯의 높이에 이미지의 넓이를 맞춤
fit:BoxFit.<strong>none</strong>-&gt; 원본 이미지 크기와 비율을 그대로 사용
fit:BoxFit.<strong>scaleDown</strong>-&gt; BoxFit.none의 설정에 이미지를 중앙 정렬 + if (부모 위젯 &lt; 이미지) 이미지 크기를 줄임</p>
</blockquote>
<blockquote>
<h4 id="systemchrome">SystemChrome</h4>
<p>: 시스템 UI를 제어하는 클래스 (상태바, 네비게이션바 등)
<strong>setSystemUIOverlayStyle()</strong>
-&gt; 상태바 스타일 바꾸는 함수
예시 코드) SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark);
// 상태바 아이콘을 어둡게 만듬</p>
</blockquote>
<h3 id="수업코드">수업코드</h3>
<p>image_carousel
<strong>home_screen.dart</strong></p>
<pre><code>import &#39;package:flutter/material.dart&#39;; //기본 라이브러리
import &#39;package:flutter/services.dart&#39;;
import &#39;dart:async&#39;;

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  State&lt;HomeScreen&gt; createState() =&gt; _HomeScreenState();
}

class _HomeScreenState extends State&lt;HomeScreen&gt; {
  final PageController pageController = PageController();

  //initState() 함수 등록
  @override
  void initState() {
    super.initState();

    Timer.periodic(Duration(seconds: 3), (timer) {
      //1. 현재 페이지 가져오기
      int? nextPage = pageController.page?.toInt();
      //2. 페이지 값이 엇을 때 예외처fl
      if (nextPage == null) {
        return;
      }
      //3. 첫 페이지와 마지막 페이지 분기 처리
      if (nextPage == 4) {
        nextPage = 0;
      } else {
        nextPage++;
      }
      //4. 페이지 변경
      pageController.animateToPage(
        nextPage,
        duration: Duration(milliseconds: 500),
        curve: Curves.ease,
      );
    });


  }
  @override
  Widget build(BuildContext context) {
    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark);
    return Scaffold(
      body: PageView(
        controller: pageController,
        children: [1, 2, 3, 4, 5]
            .map(
              (number) =&gt;
              Image.asset(&#39;asset/img/$number.jpg&#39;, fit: BoxFit.cover),
        )
            .toList(),
      ),
    );
  }
}
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter 2일차] / blog_web_app]]></title>
            <link>https://velog.io/@ttt0_0/Flutter-2%EC%9D%BC%EC%B0%A8-blogwebapp</link>
            <guid>https://velog.io/@ttt0_0/Flutter-2%EC%9D%BC%EC%B0%A8-blogwebapp</guid>
            <pubDate>Mon, 30 Mar 2026 14:13:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<pre><code>row : 가로 배치
colum : 세로 배치
callback 함수 : 나중에 실행되도록 다른 함수에 전달되는 함수
MaterialApp : flutter에 최상위의 위젯, MaterialApp안에 home 매개변수안에 입력하는 것
위젯 : 화면을 구성에 필요한 것들
Scaffold 위젯
- 상단바 : appBar
- 본문 
- 하단 버튼
- 네비게이션바
casecade operator : .. 연산자를 사용해서 특정 인스턴스의 속성이나 멤버 함수를 연속해서 사용할 수 있게 하는 연산자
build : 위젯 반환</code></pre></blockquote>
<p>  <img src="https://velog.velcdn.com/images/ttt0_0/post/2e42c959-f2d2-4df1-944b-5dfe87f68fe6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/3372b1c6-5c48-41a2-bb0f-00f1ae64edda/image.png" alt="">
➡️ 인터넷 권한</p>
<h2 id="수업코드"><strong>수업코드</strong></h2>
<h3 id="home_screendart"><strong>home_screen.dart</strong></h3>
<pre><code>import &#39;package:flutter/material.dart&#39;;
import &#39;package:webview_flutter/webview_flutter.dart&#39;;

class HomeScreen extends StatelessWidget {

  WebViewController webViewController = WebViewController()

  ..loadRequest(Uri.parse(&#39;http://blog.codefactory.ai&#39;))

  ..setJavaScriptMode(JavaScriptMode.unrestricted);

  HomeScreen({Key? key}) : super (key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(

      appBar: AppBar(
        backgroundColor: Colors.yellow,

        title: Text(&#39;다연쌤 최고&#39;),
        centerTitle: true,

      ),
      body: WebViewWidget(
        controller: webViewController, //속성에 : 를 연결해야 웹 뷰
      )
    );
  }
}</code></pre><h3 id="maindart">main.dart</h3>
<pre><code>import &#39;package:blog_web_app/screen/home_screen.dart&#39;;
import &#39;package:flutter/material.dart&#39;;
void main() {
  runApp(
    MaterialApp(
      home: HomeScreen(),

    ),
  );
}
</code></pre><h3 id="결과물">결과물</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/c7685e21-ec44-4b7a-8ea1-0b03950da738/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter 1일차]]]></title>
            <link>https://velog.io/@ttt0_0/Flutter</link>
            <guid>https://velog.io/@ttt0_0/Flutter</guid>
            <pubDate>Tue, 24 Mar 2026 15:21:07 GMT</pubDate>
            <description><![CDATA[<ul>
<li><p><strong>데이터 타입</strong></p>
<ul>
<li><p><code>var</code> : 변수 타입을 처음에 추론해서 고정시킴</p>
</li>
<li><p><code>dynamic</code> : 변수 타입 변경 가능</p>
</li>
<li><p><code>final</code>(상수) : 런타임 상수 - 실행될 때 값이 확정</p>
</li>
<li><p><code>const</code> (상수) : 빌드 타임 상수 - 코드를 실행하지 않은 상태에서 값이 확정</p>
</li>
<li><p><code>int / double / String / bool</code> : 정수 / 실수 / 문자열 / 논리형 자료형</p>
</li>
<li><p><code>Collection</code> : 여러 값을 하나의 변수에 저장할 수 있는 타입입니다.</p>
</li>
<li><p><code>List</code>: 순서대로 저장</p>
</li>
<li><p><code>Map</code> : 특정 키 값을 기반으로 빠르게 값을 검색</p>
</li>
<li><p><code>Set</code>  : 중복 값을 허용x</p>
<pre><code>     .</code></pre></li>
</ul>
</li>
</ul>
<ul>
<li><p><strong>키워드</strong></p>
<ul>
<li><p><code>null</code> 관련 연산자</p>
</li>
<li><p><code>required</code>  : 네임드 파라미터에서 사용, 해당 값을 <strong>반드시 전달</strong>한다고 강제</p>
<p>.</p>
</li>
</ul>
</li>
<li><p><strong>함수 정리</strong></p>
<ul>
<li><p>main() {} : 메인 메소드</p>
</li>
<li><p>print() : 출력 메소드</p>
</li>
<li><p>reduce() : 값을 순회하면서 매개변수에 입력된 함수를 실행</p>
</li>
<li><p>where() :</p>
<pre><code>final illitList = iveList.where(
   (name) =&gt; name == &#39;리즈&#39; || name == &#39;원영&#39;,</code></pre><p>  이름이 리즈이거나 원영인것만 true로 남기고 나머지 탈락</p>
<pre><code>);</code></pre></li>
</ul>
</li>
</ul>
<h3 id="위젯">위젯</h3>
<ul>
<li>자식을 하나만 갖는 위젯<ol>
<li>Container : 자식을 담는 컨테이너 역할(배경색, 너비, 높이, 테두리 등 디자인)</li>
<li>GestureDetector : 플러터에서 제공하는제스처 기능을 자식 위젯에서 인식</li>
<li>SizeBox : 높이, 너비 지정하는 위젯 — 디자인 X, </li>
</ol>
</li>
<li>자식을 여러 갖는 위젯<ol>
<li>Colum : children 매개변수에 입력된 모든 위젯을 세로로 배치</li>
<li>Row : children 매개변수에 입력된 모든 위젯을 가로로 배치</li>
<li>ListView : 리스트를 구현할 때 사용 — 입력된 위젯이 화면을 벗어나게되면 스크롤 가능</li>
</ol>
</li>
<li>Text() 위젯</li>
</ul>
<h3 id="함수">함수</h3>
<blockquote>
<p>onPanStart : 수평 또는 수직 드래그가 시작되었을 때 함수 실행</p>
</blockquote>
<blockquote>
<p>onPanUpdate : 수평 또는 수직 드래그하는 동안 드래그 위치가 업데이트 될 때 마다 실행 </p>
</blockquote>
<blockquote>
<p>onPanEnd : 수평 또는 수직 드래그가 끝났을 때 실행</p>
</blockquote>
<blockquote>
<p>onHorizontalDragStart : 수평드래그가 실행될 떄</p>
</blockquote>
<blockquote>
<p>onHorizontalDragUpdate : 수평드래그를 하는 동안</p>
</blockquote>
<blockquote>
<p>onHorizontalDragEnd : 수평드래그가 끝났을 때</p>
</blockquote>
<blockquote>
<p>onVerticalDragStart : 수직드래그가 시작할 때</p>
</blockquote>
<blockquote>
<p>onVerticalDragUpdate : 수직드래그를 하는 동안</p>
</blockquote>
<blockquote>
<p>onVerticalDragEnd : 수직드래그가 끝났을 때</p>
</blockquote>
<blockquote>
<p>onScaleStart : 확대가 시작됐을 때</p>
</blockquote>
<blockquote>
<p>onScaleUpdate : 확대가 진행되는 동안</p>
</blockquote>
<blockquote>
<p>onScaleEnd : 확대가 끝났을 때</p>
</blockquote>
<blockquote>
<p>ElevatedButton - 튀어나온 버튼</p>
</blockquote>
<blockquote>
<p>TextButton - 텍스트 버튼</p>
</blockquote>
<blockquote>
<p>IconButton - 아이콘 버튼</p>
</blockquote>
<blockquote>
<p>Border.all - 테두리</p>
</blockquote>
<blockquote>
<p>borderRadius - 둥글게</p>
</blockquote>
<blockquote>
<p>padding - 안쪽 여백</p>
</blockquote>
<blockquote>
<p>margin - 바깥쪽 여백</p>
</blockquote>
<blockquote>
<p>SizedBox - 크기 제한</p>
</blockquote>
<blockquote>
<p>SafeArea - 화면이 잘리지 않게 보호해주는 위젯</p>
</blockquote>
<h3 id="수업코드-사진">수업코드 사진</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/b9f378c6-d68d-4cbb-80b5-6783a23c14c6/image.png" alt="">
<img src="https://velog.velcdn.com/images/ttt0_0/post/42c59544-7478-4a33-b857-2d1b85e5f04a/image.png" alt="">
<img src="https://velog.velcdn.com/images/ttt0_0/post/a652fb99-d322-43c1-8b1a-82946a6cd3ff/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[💻 컬러 선택으로 배경 바꾸기]]></title>
            <link>https://velog.io/@ttt0_0/%EC%BB%AC%EB%9F%AC-%EC%84%A0%ED%83%9D%EC%9C%BC%EB%A1%9C-%EB%B0%B0%EA%B2%BD-%EB%B0%94%EA%BE%B8%EA%B8%B0</link>
            <guid>https://velog.io/@ttt0_0/%EC%BB%AC%EB%9F%AC-%EC%84%A0%ED%83%9D%EC%9C%BC%EB%A1%9C-%EB%B0%B0%EA%B2%BD-%EB%B0%94%EA%BE%B8%EA%B8%B0</guid>
            <pubDate>Wed, 25 Feb 2026 17:57:39 GMT</pubDate>
            <description><![CDATA[<h3 id="🔽결과물🔽">🔽결과물🔽</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/5d294179-d725-4060-9a53-07c01a8c4c7f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/876792f8-ab2d-4f1e-89d1-385db3bc7e2e/image.png" alt=""></p>
<h4 id="우연히-short를-보다가-발견했다-html-css-js로-간단하게-만들-수-있을-것-같아서-바로-시작했다">우연히 short를 보다가 발견했다. HTML, CSS, JS로 간단하게 만들 수 있을 것 같아서 바로 시작했다.</h4>
<p>HTML과 CSS는 방과후 수업에서 많이 접해봐서 익숙했다.
JS는 접해본 경험이 몇번 없어서 이번 기회로 조금 더 학습하는 계기가 된 것 같다.</p>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/2bd3891b-544a-4768-8d17-9b37f03fcef0/image.png" alt=""></p>
<blockquote>
<p><strong>let</strong>은 JS에서 변수 선언을 하고 업데이트는 가능하지만, 재선언은 불가능한 블록 변수</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[🩵[HTML 4일차]]]></title>
            <link>https://velog.io/@ttt0_0/HTML-4%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@ttt0_0/HTML-4%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 08 Sep 2025 15:51:57 GMT</pubDate>
            <description><![CDATA[<h2 id="1️⃣-form-태그">1️⃣ &lt;form&gt; 태그</h2>
<blockquote>
<pre><code>&lt;label for = &#39;grade&#39;&gt;학년&lt;/label&gt;&lt;br&gt;
        &lt;select name = &quot;grade&quot;&gt;
            &lt;option value = &quot;선택하세요&quot; required&gt;선택하세요.&lt;/option&gt;
            &lt;option value = &quot;1&quot;&gt; 1학년&lt;/option&gt;
            &lt;option value = &quot;2&quot;&gt; 2학년&lt;/option&gt;
            &lt;option value = &quot;3&quot;&gt; 3학년&lt;/option&gt;
        &lt;/select&gt;&lt;br&gt;
&lt;label for = &quot;tel&quot;&gt;전화번호&lt;/label&gt;
&lt;input type = &quot;tel&quot; name = &quot;phone&quot; pattern = &quot;^010-[0-9]{4}-[0-9]{4}$&quot; placeholder=&quot;010-1234-1234&quot;&gt;</code></pre></blockquote>
<pre><code>
&gt; ![](https://velog.velcdn.com/images/ttt0_0/post/7600784e-5e15-4905-a508-b486b881b6e6/image.png)
![](https://velog.velcdn.com/images/ttt0_0/post/539243f3-2776-41c1-8329-6c91c81cfa62/image.png)

## 2️⃣ &lt;태그&gt;
&gt; ```html
&gt; - ✅ &lt;p&gt; : 하나의 문단(블록 요소, 자동으로 줄바꿈)
- ✅ &lt;br&gt; : 줄바꿈
- ✅ &lt;!--주석 내용--&gt; : 주석 처리
- ✅ &lt;html&gt;&lt;/html&gt; : HTML문서의 시작과 끝
- ✅ &lt;a&gt;, &lt;span&gt; : 인라인 요소 (공간이 부족하면 밑으로 줄바꿈)
- ✅ &lt;hN&gt; : 글씨 크기
- ✅ &lt;div&gt; : container로 사용하는 태그 (의미X, CSS 적용시 레이아웃 나눔으로
디자인 다르게 적용)
- ✅ &lt;ol&gt; : 순서 있는 목록 (1.--- 2.--- 3.---)
- ✅ &lt;ul&gt; : 순서 없는 목록 (-... -... -...)
- ✅ &lt;li&gt; : 목록 항목 (&lt;ol&gt;, &lt;ul&gt; 각 항목을 정의할 때 사용)
- ✅ &lt;a href = &quot;링크&quot;&gt;home&lt;/a&gt; : 하이퍼링크 구문 
- ✅ &lt;img src = &quot;이미지 경로&quot; alt = &quot;이미지 설명 텍스트&quot;&gt; : 이미지 삽입
- ✅ &lt;input type = &quot;password&quot;&gt; : 비밀번호 입력창 (····)
- ✅ &lt;input type = &quot;number&quot;&gt; : 숫자 하나씩 증가 또는 감소
- ✅ &lt;input type = &quot;checkbox&quot;&gt; : 중복 선택 가능한 체크박스
- ✅ &lt;input type = &quot;radio&quot;&gt; : 같은 lable로 묶었을 때 중복 선택 불가한 체크박스
- ✅ &lt;input type = &quot;file&quot;&gt; : 파일 가져오기
- ✅ &lt;input type = &quot;submit&quot;&gt; : 제출 버튼
- ✅ &lt;input type = &quot;reset&quot;&gt; : 초기화 버튼
- ✅ &lt;label for = &quot;usergender&quot;&gt;성별&lt;/label&gt; : lable로 태그 요소(input)를 묶음으로써 설명(연결하려는 속성 같게하기)
- ⭐️ required : 필수 입력
- ⭐️ &lt;pattern = &quot;^010-[0-9]{4}-[0-9]{4}$&quot;&gt; : 전화번호 형식을 검사하는 pattern 속성 문법 (^ : 문자열 시작, [0-9]{4} : 0부터 9안에서 숫자 4자 $ : 문자열 끝)
- ⭐️ placeholder=&quot;010-1234-1234&quot;&gt; : 입력 예시
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[🩵[HTML 3일차]]]></title>
            <link>https://velog.io/@ttt0_0/HTML-3%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@ttt0_0/HTML-3%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Tue, 02 Sep 2025 14:56:03 GMT</pubDate>
            <description><![CDATA[<h2 id="1️⃣form-태그">1️⃣&lt;form&gt; 태그</h2>
<blockquote>
<pre><code>&lt;input type = &quot;text&quot;&gt;        입력창
&lt;input type = &quot;password&quot;&gt;    패스워드 입력창 (입력내용 숨김)
&lt;input type = &quot;number&quot;&gt;        숫자 입력(증가 감소)
&lt;input type = &quot;checkbox&quot;&gt;    중복 선택 가능
&lt;input type = &quot;radio&quot;&gt;        중복 선택 불가
&lt;input type = &quot;file&quot;&gt;        파일 선택
&lt;input type = &quot;submit&quot;&gt;        제출 버튼
&lt;input type = &quot;reset&quot;&gt;        초기화 버튼</code></pre></blockquote>
<pre><code>⭐️결과⭐️
&lt;input type = &quot;text&quot;&gt;  입력창
&lt;input type = &quot;password&quot;&gt;  패스워드 입력창 (입력내용 숨김)
&lt;input type = &quot;number&quot;&gt; 숫자 입력(증가 감소)
&lt;input type = &quot;checkbox&quot;&gt;     중복 선택 가능
&lt;input type = &quot;radio&quot;&gt;        중복 선택 불가
&lt;input type = &quot;file&quot;&gt;         파일 선택
&lt;input type = &quot;submit&quot;&gt;       제출 버튼
 &lt;input type = &quot;reset&quot;&gt;       초기화 버튼

### 2️⃣ 적용
&gt; ![](https://velog.velcdn.com/images/ttt0_0/post/38f4db38-6cee-4a62-a71b-a43048eeda47/image.png)

## 3️⃣ &lt;태그&gt;
&gt; ```html
&gt; - ✅ &lt;p&gt; : 하나의 문단(블록 요소, 자동으로 줄바꿈)
- ✅ &lt;br&gt; : 줄바꿈
- ✅ &lt;!--주석 내용--&gt; : 주석 처리
- ✅ &lt;html&gt;&lt;/html&gt; : HTML문서의 시작과 끝
- ✅ &lt;a&gt;, &lt;span&gt; : 인라인 요소 (공간이 부족하면 밑으로 줄바꿈)
- ✅ &lt;hN&gt; : 글씨 크기
- ✅ &lt;div&gt; : container로 사용하는 태그 (의미X, CSS 적용시 레이아웃 나눔으로
디자인 다르게 적용)
- ✅ &lt;ol&gt; : 순서 있는 목록 (1.--- 2.--- 3.---)
- ✅ &lt;ul&gt; : 순서 없는 목록 (-... -... -...)
- ✅ &lt;li&gt; : 목록 항목 (&lt;ol&gt;, &lt;ul&gt; 각 항목을 정의할 때 사용)
- ✅ &lt;a href = &quot;링크&quot;&gt;home&lt;/a&gt; : 하이퍼링크 구문 
- ✅ &lt;img src = &quot;이미지 경로&quot; alt = &quot;이미지 설명 텍스트&quot;&gt; : 이미지 삽입
- ⭐️ &lt;input type = &quot;password&quot;&gt; : 비밀번호 입력창 (····)
- ⭐️ &lt;input type = &quot;number&quot;&gt; : 숫자 하나씩 증가 또는 감소
- ⭐️ &lt;input type = &quot;checkbox&quot;&gt; : 중복 선택 가능한 체크박스
- ⭐️ &lt;input type = &quot;radio&quot;&gt; : 같은 lable로 묶었을 때 중복 선택 불가한 체크박스
- ⭐️ &lt;input type = &quot;file&quot;&gt; : 파일 가져오기
- ⭐️ &lt;input type = &quot;submit&quot;&gt; : 제출 버튼
- ⭐️ &lt;input type = &quot;reset&quot;&gt; : 초기화 버튼
- ⭐️ &lt;label for = &quot;usergender&quot;&gt;성별&lt;/label&gt; : lable로 태그 요소(input)를 묶음으로써 설명(연결하려는 속성 같게하기)

## 4️⃣ 활용
&gt; ![](https://velog.velcdn.com/images/ttt0_0/post/3e8b699b-2be3-4668-bf75-fce53b1ea2d5/image.png)



</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[🩵[HTML 2일차]]]></title>
            <link>https://velog.io/@ttt0_0/HTML-2%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@ttt0_0/HTML-2%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Thu, 28 Aug 2025 15:46:47 GMT</pubDate>
            <description><![CDATA[<h2 id="1️⃣-태그">1️⃣ &lt;태그&gt;</h2>
<blockquote>
<pre><code class="language-html">- ✅ &lt;p&gt; : 하나의 문단(블록 요소, 자동으로 줄바꿈)</code></pre>
</blockquote>
<ul>
<li>✅ <br> : 줄바꿈</li>
<li>✅ <!--주석 내용--> : 주석 처리</li>
<li>✅ <html></html> : HTML문서의 시작과 끝</li>
<li>✅ <a>, <span> : 인라인 요소 (공간이 부족하면 밑으로 줄바꿈)</li>
<li>✅ <hN> : 글씨 크기</li>
<li>✅ <div> : container로 사용하는 태그 (의미X, CSS 적용시 레이아웃 나눔으로
디자인 다르게 적용)</li>
<li>✅ <ol> : 순서 있는 목록 (1.--- 2.--- 3.---)</li>
<li>✅ <ul> : 순서 없는 목록 (-... -... -...)</li>
<li>✅ <li> : 목록 항목 (<ol>, <ul> 각 항목을 정의할 때 사용)</li>
<li>✅ <a href = "링크">home</a> : 하이퍼링크 구문 </li>
<li>⭐️ <img src = "이미지 경로" alt = "이미지 설명 텍스트"> : 이미지 삽입<pre><code></code></pre></li>
</ul>
<h3 id="2️⃣-실습">2️⃣ 실습</h3>
<blockquote>
<pre><code class="language-html">  &lt;!DOCTYPE html&gt;</code></pre>
</blockquote>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>연습2</title>
</head>
<body>
    <img src = "윌리엄.jpg" width = "300" alt = "아기">
</body>
</html>

<p>  width = &quot;&quot; -&gt; 사진 크기 조절</p>
<h3 id="3️⃣-결과">3️⃣ 결과</h3>
<blockquote>
<p>  <img src="https://velog.velcdn.com/images/ttt0_0/post/eabc193f-4eac-4559-96d2-2d3db482a141/image.png" alt=""></p>
</blockquote>
<pre><code>
###   4️⃣ Tip💡
&gt;   - alt = &quot;a&quot; -&gt; 사진이 나오지 않을때 a로 대체
  - 넣을 사진은 **꼭 같은 폴더에 위치**</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[🩵[HTML 1일차]]]></title>
            <link>https://velog.io/@ttt0_0/HTML-1%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@ttt0_0/HTML-1%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 27 Aug 2025 14:35:03 GMT</pubDate>
            <description><![CDATA[<h2 id="1️⃣html-기본-구조">1️⃣HTML 기본 구조</h2>
<blockquote>
<pre><code class="language-html">&lt;!DOCTYPE html&gt; 
    &lt;head&gt;
      &lt;title&gt;
        문서의 제목
      &lt;/title&gt;
    &lt;/head&gt;
    &lt;body&gt;
      안녕하세요
    &lt;/body&gt;</code></pre>
</blockquote>
</html>
```

<h3 id="2️⃣결과">2️⃣결과</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/7be8958f-d7ce-4847-b7a7-28605d934fd5/image.png" alt=""></p>
</blockquote>
<h3 id="3️⃣태그">3️⃣&lt;태그&gt;</h3>
<blockquote>
<pre><code class="language-html">- &lt;p&gt; : 하나의 문단(블록 요소, 자동으로 줄바꿈)</code></pre>
</blockquote>
<ul>
<li><br> : 줄바꿈</li>
<li><!--주석 내용--> : 주석 처리</li>
<li><html></html> : HTML문서의 시작과 끝</li>
<li><a>, <span> : 인라인 요소 (공간이 부족하면 밑으로 줄바꿈)</li>
<li><hN> : 글씨 크기</li>
<li><div> : container로 사용하는 태그 (의미X, CSS 적용시 레이아웃 나눔으로
디자인 다르게 적용)</li>
<li><ol> : 순서 있는 목록 (1.--- 2.--- 3.---)</li>
<li><ul> : 순서 없는 목록 (-... -... -...)</li>
<li><li> : 목록 항목 (<ol>, <ul> 각 항목을 정의할 때 사용)</li>
<li><a href = "링크">home</a> : 하이퍼링크 구문 <pre><code></code></pre></li>
</ul>
<h3 id="4️⃣단축키">4️⃣&lt;단축키&gt;</h3>
<blockquote>
<p>alt + shift + 방향키 : -방향으로 복붙</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[📚[MySQL 2~3일차]]]></title>
            <link>https://velog.io/@ttt0_0/MySQL-23%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@ttt0_0/MySQL-23%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 18 Aug 2025 13:59:28 GMT</pubDate>
            <description><![CDATA[<h2 id="1️⃣-실습">1️⃣ 실습</h2>
<h3 id="✅-별칭-지정">✅ 별칭 지정(&quot;&quot;)</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/4b6113bf-a9c5-41ce-ad9a-016014bf6972/image.png" alt=""></p>
<h3 id="✅-desc내림차순">✅ DESC(내림차순)</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/56ec5a14-0bad-4695-bc21-1a380fd5572c/image.png" alt=""></p>
<h3 id="✅-별칭-지정--group-by--having">✅ 별칭 지정(&quot;&quot;) + GROUP BY + HAVING</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/b59c9138-d111-49db-b3a9-c3a767be39e7/image.png" alt=""></p>
<h3 id="✅-count">✅ COUNT()</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/9047dc91-d580-47dd-84e5-e0c980268537/image.png" alt=""></p>
<h3 id="✅-avg--별칭-지정--group-by">✅ AVG() + 별칭 지정(&quot;&quot;) + GROUP BY</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/568b95b7-4c12-4977-b5cc-d53bbdd3b29a/image.png" alt=""></p>
<h3 id="✅-group-by--sum">✅ GROUP BY + SUM()</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/50ecfe87-13d4-4081-a3f1-d28623a59fd3/image.png" alt=""></p>
<h3 id="✅-distinct">✅ DISTINCT</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/ca2fc8ba-41bc-4ca6-b484-b9d1dbbed71f/image.png" alt=""></p>
<h3 id="✅-limit">✅ LIMIT</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/6a55d258-cf64-4a0a-bf98-139adf7ae6e6/image.png" alt=""></p>
<h3 id="✅-use--order-by">✅ USE + ORDER BY</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/c15bcf06-d87e-499d-bdc2-e8fd6d977ffa/image.png" alt=""></p>
<h3 id="✅-평균-조회">✅ 평균 조회</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/a42a6dc6-0c73-43a6-82e9-434234a5a8d2/image.png" alt=""></p>
<h3 id="✅-like--__핑크">✅ LIKE + __핑크</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/296656cb-dd3f-49a5-b04b-b4f31cb87f9b/image.png" alt=""></p>
<h3 id="✅-like--우">✅ LIKE + 우%</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/84238632-5552-4d6f-a414-2c2af12af4bf/image.png" alt=""></p>
<h3 id="✅-in">✅ IN</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/c6e6455e-652e-4ed9-986b-ee604a68faa6/image.png" alt=""></p>
<h3 id="✅-or">✅ OR</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/cdc43e9d-4e88-4604-91ed-4d3aa67b52a0/image.png" alt=""></p>
<h3 id="✅-between--and">✅ BETWEEN ~ AND</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/c1668859-0139-4ee4-ac40-63d044605b71/image.png" alt=""></p>
<h3 id="✅-and-연산자">✅ AND 연산자</h3>
<p><img src="https://velog.velcdn.com/images/ttt0_0/post/6d1f8777-f51b-44c4-ab75-9d095cfac551/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[📚[MySQL 1일차]]]></title>
            <link>https://velog.io/@ttt0_0/MySQL-1%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@ttt0_0/MySQL-1%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Sun, 17 Aug 2025 14:09:21 GMT</pubDate>
            <description><![CDATA[<h2 id="1️⃣-dbdatabase란">1️⃣ DB(Database)란?</h2>
<blockquote>
<p><strong>DB</strong>란? 구조화된 정보나 데이터를 체계적으로 모아둔 집합체.
보통 컴퓨터 시스템에 전자적으로 저장되며, DBMS와 함께 사용될 때 데이터베이스 시스템이라고 부른다.</p>
</blockquote>
<h2 id="2️⃣-dbmsdatabase-management-system란">2️⃣ DBMS(Database Management System)란?</h2>
<blockquote>
<p>데이터베이스를 관리하고 운영하는 소프트웨어이다. </p>
</blockquote>
<h2 id="3️⃣-db의-특징">3️⃣ DB의 특징</h2>
<blockquote>
<ul>
<li>데이터의 독립성: 데이터의 구조나 저장 방식이 변경되어도 응용 프로그램에는 영향을 주지 않음</li>
</ul>
</blockquote>
<ul>
<li>통합된 데이터: 여러 곳에서 사용되던 데이터를 하나로 통합하여 저장</li>
<li>공용 데이터: 여러 사용자가 각각 다른 목적으로 데이터베이스의 데이터를 공동 이용함</li>
</ul>
<h2 id="4️⃣-테이블">4️⃣ 테이블</h2>
<blockquote>
<p>데이터베이스의 최소 단위로 하나 이상의 열과 행으로 구성 (<strong>표</strong>)</p>
</blockquote>
<h2 id="5️⃣-mysql-예약어">5️⃣ MySQL 예약어</h2>
<blockquote>
<p><strong>SELECT</strong> -&gt; 조회할 데이터 지정
<strong>FROM</strong> -&gt; 테이블 지정
<strong>WHERE</strong> -&gt; 조건문 (ORDER BY 절 앞에 배치)
<strong>ORDER BY</strong> -&gt; 정렬 (기본 값: 오름차순)
<strong>ASC</strong> -&gt; 오름차순
<strong>DESC</strong> -&gt; 내림차순
<strong>CREATE</strong> -&gt; 데이터베이스 개체 생성
<strong>DROP</strong> -&gt; 데이터베이스 개체 삭제</p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>