일정관리 앱 만들기 (table_calendar)

잠만보·2024년 10월 5일

사전 지식

table_calendar 플러그인

달력을 쉽게 구현할 수 있도록 해주는 플러그인이다.

사전 준비

pubspec.yaml 설정하기

지금까지 플러그인을 추가할 때 pubspec.yaml 파일의 dependencies 에 추가했었다.
pubspec.yaml 파일에는 플러그인을 추가할 수 있는 키값이 하나 더 있는데 바로 dev_dependencies 이다.

dev_dependencies 키워드는 dependencies 키에 작성된 플러그인들과 달리 앱과 함께 패키징 되지 않는다.

따라서 개발할때만 필요하고 앱을 실행할 때 필요없는 플러그인들은 dev_dependencies 키에 입력하면 된다.

레이아웃

홈스크린 레이아웃

  1. 달력에서 특정 날짜를 선택하면 날짜에 해당되는 달력 아래에 일정 리스트가 나타난다.

  2. 일정은 시작시간 기준으로 오름차순 정렬되고 달력과 리스트 사이에는 선택 날짜에 해당하는 일정이 몇개인지 보여주는 배너가 있다.

  3. 우측 하단에 일정 추가버튼이 있고 누르면 일정 생성 화면이 뜬다.

일정 생성/추가 시 화면 레이아웃

구현하기

주 색상 설정하기

lib/const/colors.dart 파일에 주로 사용할 색상을 지정한다.

달력 구현하기 & 적용하기

HomeScreen 화면 윗부분의 달력을 MainCalendar 클래스로 만들겠다.
달력 기능은 미리 추가한 talbe_calendar 플러그인을 사용한다.

import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';

class MainCalendar extends StatelessWidget {
  const MainCalendar({super.key});

  @override
  Widget build(BuildContext context) {
    return TableCalendar(
      focusedDay: DateTime.now(), // 화면에 보여지는 날
      firstDay: DateTime(1800, 1, 1), // 달력 첫째 날
      lastDay: DateTime(3000, 1, 1), // 달력 마지막 날
    );
  }
}

이제 만든 달력 클래스를 적용해보자

import 'package:calendar_scheduler/component/main_calendar.dart';
import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea( // 시스템 UI 피해서 UI 구현
        child: Column( // 달력과 리스트를 세로로 배치
          children: [
            MainCalendar(), // 달력 위젯 렌더링
          ],
        ),
      ),
    );
  }
}

달력 스타일링 하기

import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';

class MainCalendar extends StatelessWidget {
  const MainCalendar({super.key});

  @override
  Widget build(BuildContext context) {
    return TableCalendar(
      focusedDay: DateTime.now(),
      // 화면에 보여지는 날
      firstDay: DateTime(1800, 1, 1),
      // 달력 첫째 날
      lastDay: DateTime(3000, 1, 1),
      // 달력 마지막 날
      headerStyle: HeaderStyle( // 1. 달력 최상단 스타일
        titleCentered: true,  // 제목 중앙 위치
        formatButtonVisible: false, // 달력 크기 선택 옵션 제거
        titleTextStyle: TextStyle( // 제목글꼴
          fontWeight: FontWeight.w700,
          fontSize: 16.0,
        ),
      ),
      calendarStyle: CalendarStyle(
        isTodayHighlighted: false,
        defaultDecoration: BoxDecoration( // 2. 기본 날짜 스타일
          borderRadius: BorderRadius.circular(6.0),
          color: LIGHT_GREY_COLOR,
        ),
        weekendDecoration: BoxDecoration( // 3. 주말 날짜 스타일
          borderRadius: BorderRadius.circular(6.0),
          color: DARK_GREY_COLOR,
        ),
        selectedDecoration: BoxDecoration( // 4. 선택된 날짜 스타일
          borderRadius: BorderRadius.circular(6.0),
          border: Border.all(
            color: PRIMARY_COLOR,
            width: 1.0,
          ),
        ),
        defaultTextStyle: TextStyle( // 5. 기본 글꼴
          fontWeight: FontWeight.w600,
          color: DARK_GREY_COLOR,
        ),
        weekendTextStyle: TextStyle( // 6. 주말 글꼴
          fontWeight: FontWeight.w600,
          color: DARK_GREY_COLOR,
        ),
        selectedTextStyle: TextStyle( // 7. 선택된 날짜 글꼴
          fontWeight: FontWeight.w600,
          color: PRIMARY_COLOR,
        ),
      ),
    );
  }
}

달력에 기능 부여하기

날짜가 선택될 때 마다 실행되는 콜백함수를 등록하고 추가적으로 어떤 날짜가 현재 선택된 상태인지 달력에 표시해주어야 한다.

HomeScreen 에서 모든 상태를 관리할거기 때문에 MainCalendar 에 관련 코드를 작성한다.

import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';

class MainCalendar extends StatelessWidget {
  final OnDaySelected onDaySelected; // 1. 날짜 선택 시 실행할 함수
  final DateTime selectedDate; // 2. 선택된 날짜

  // 생성자
  const MainCalendar({
    super.key,
    required this.onDaySelected,
    required this.selectedDate,
  });

  @override
  Widget build(BuildContext context) {
    return TableCalendar(
      onDaySelected: onDaySelected,
      // 3. 날짜 선택 시 실행할 함수
      selectedDayPredicate: (date) => // 4. 선택된 날짜를 구분할 로직, 어떤 날짜를 선택된 날짜로 지정할지 결정
          date.year == selectedDate.year &&
          date.month == selectedDate.month &&
          date.day == selectedDate.day,
      focusedDay: DateTime.now(),
      
      .
      .
      .
      
      ),
    );
  }
}

HomeScreen 에 적용해보겠다.

import 'package:calendar_scheduler/component/main_calendar.dart';
import 'package:flutter/material.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  DateTime selectedDate = DateTime.utc( // 선택된 날짜를 관리할 변수
    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        // 시스템 UI 피해서 UI 구현
        child: Column(
          // 달력과 리스트를 세로로 배치
          children: [
            MainCalendar(
              selectedDate: selectedDate, // 선택된 날짜 전달하기
              onDaySelected: onDaySelected, // 날짜가 선택됬을 떄 실행할 함수
            ), // 달력 위젯 렌더링
          ],
        ),
      ),
    );
  }

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

이제 달력을 누르면 날짜선택이 잘 된다.

선택된 날의 일정을 보여주기: Scheduled 위젯

선택된 날짜에 해당하는 일정을 보여줄 ScheduleCard 위젯을 만들겠다.

시작시간과 종료시간, 내용으로 구성된다

Time, Content 위젯 만들기

import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';

// 시간을 렌더링할 Time 위젯
class _Time extends StatelessWidget {
  final int startTime; // 시작시간
  final int endTime; // 종료시간

  // 생성자
  const _Time({
    super.key,
    required this.endTime,
    required this.startTime,
  });

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

    return Column(
      // 시간 위에서 아래로 시작시간, 끝나는시간 배치
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 각각 숫자가 두자리수가 안되면 0으로 채워주기
        Text(
          '${startTime.toString().padLeft(2, '0')}:00',
          style: textStyle,
        ),
        Text(
          '${endTime.toString().padLeft(2, '0')}:00',
          style: textStyle.copyWith(fontSize: 10.0),
        ),
      ],
    );
  }
}

// 내용을 렌더링할 Content 위젯
class _Content extends StatelessWidget {
  final String content;

  // 생성자
  const _Content({
    super.key,
    required this.content,
  });

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

ScheduledCard 위젯 만들기

// Schedule Card 위젯
class ScheduleCard extends StatelessWidget {
  final int startTime;
  final int endTime;
  final String content;

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

  @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.0),
        child: IntrinsicHeight( // 1. 높이를 내부 위젯들의 최대 높이로 설정
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              // 일정 시작과 종료 시간을 알려줄 Time 위젯
              _Time(
                startTime: startTime,
                endTime: endTime,
              ),
              SizedBox(
                width: 16.0,
              ),
              // 일정 내용을 보여줄 Content 위젯
              _Content(
                content: content,
              ),
              SizedBox(
                width: 16.0,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

HomeScreen 에 ScheduleCard 위젯 적용하기

import 'package:calendar_scheduler/component/main_calendar.dart';
import 'package:calendar_scheduler/component/schedule_card.dart';
import 'package:flutter/material.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  DateTime selectedDate = DateTime.utc(
    // 선택된 날짜를 관리할 변수
    DateTime.now().year,
    DateTime.now().month,
    DateTime.now().day,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        // 시스템 UI 피해서 UI 구현
        child: Column(
          // 달력과 리스트를 세로로 배치
          children: [
            MainCalendar(
              selectedDate: selectedDate, // 선택된 날짜 전달하기
              onDaySelected: onDaySelected, // 날짜가 선택됬을 떄 실행할 함수
            ), // 달력 위젯 렌더링
      //====================================================
            ScheduleCard(
              startTime: 12,
              endTime: 14,
              content: '프로그래밍 공부',
      //====================================================
            ),
          ],
        ),
      ),
    );
  }

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

오늘 날짜를 보여주기: TodayBanner 위젯

import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';

class TodayBanner extends StatelessWidget {
  final DateTime selectedDate; // 선택된 날짜
  final int count; // 일정의 개수

  // 생성자
  const TodayBanner({
    super.key,
    required this.selectedDate,
    required this.count,
  });

  @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.0, vertical: 8.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text( // 년 월 일 형태로 표시
              '${selectedDate.year}년 ${selectedDate.month}월 ${selectedDate
                  .day}일',
              style: textStyle,
            ),
            Text( //일정 개수 표시
              '$count개',
              style: textStyle,
            )
          ],
        ),
      ),
    )
  }
}

TodayBanner 위젯 홈스크린에 적용하기


.
.
.

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        // 시스템 UI 피해서 UI 구현
        child: Column(
          // 달력과 리스트를 세로로 배치
          children: [
            MainCalendar(
              selectedDate: selectedDate, // 선택된 날짜 전달하기
              onDaySelected: onDaySelected, // 날짜가 선택됬을 떄 실행할 함수
            ), // 달력 위젯 렌더링
            SizedBox(
              height: 8.0,
            ),
      //================================================
            TodayBanner(
              selectedDate: selectedDate,
              count: 0,
      //================================================
            ),
            ScheduleCard(
              startTime: 12,
              endTime: 14,
              content: '프로그래밍 공부',
            ),
          ],
        ),
      ),
    );
  }

달력과 할일 사이에 잘 추가가 된 것을 알 수 있다.

일정 입력하기

텍스트 필드 3개와 버튼 하나로 구성되어있다.

UI 틀 구현하기

import 'package:flutter/material.dart';

class ScheduleBottomSheet extends StatefulWidget {
  const ScheduleBottomSheet({super.key});

  @override
  State<ScheduleBottomSheet> createState() => _ScheduleBottomSheetState();
}

class _ScheduleBottomSheetState extends State<ScheduleBottomSheet> {
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Container(
        height: MediaQuery.of(context).size.height / 2, // 화면에 반을 차지하는 흰색 컨테이너 틀
        color: Colors.white,
      ),
    );
  }
}

SafeArea 위젯에 화면의 반을 차지하는 흰색 Container 위젯을 배치했다.

FloatingActionButton 누르면 입력창 나오도록 하기

.
.
.

class _HomeScreenState extends State<HomeScreen> {
  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(
            // BottomSheet 열기
            context: context,
            isDismissible: true, // 배경 탭했을 때 BottomSheet 닫기
            builder: (_) => ScheduleBottomSheet(), // 할 일 입력창 렌더링
          );
        },
        child: Icon( // '+' 모양 아이콘
          Icons.add,
        ),
      ),
      //====================================================================
     
     .
     .
     .
}

일정 내용 필드 구현하기

ScheduleBottomSheet 에 들어갈 텍스트 필드 구현하기

import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';

class CustomTextField extends StatelessWidget {
  final String label;

  const CustomTextField({
    super.key,
    required this.label,
  });

  @override
  Widget build(BuildContext context) {
    return Column( // 세로로 텍스트와 텍스트 필드 배치
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [

        // 라벨 텍스트
        Text(
          label,
          style: TextStyle(
            color: PRIMARY_COLOR,
            fontWeight: FontWeight.w600,
          ),
        ),

        // 텍스트 필드
        TextFormField(), // 폼 안에서 텍스트 필드를 쓸 때 사용
      ],
    );
  }
}

플러터에서 텍스트 필드는 TextField 위젯TextFormField 으로 나눌 수 있다.

TextField 위젯 은 각 텍스트 필드가 독립된 형태일 때 많이 사용되고

TextFormField 는 여러개의 텍스트 필드를 하나의 폼으로 제어할 때 사용된다.

'저장' 버튼을 눌렀을 때 시간 텍스트 필드 2개와 내용 텍스트 필드 하나를 제어할 계획이니 TextFormField 를 사용하겠다.

텍스트 필드 테스트

import 'package:calendar_scheduler/component/custom_text_field.dart';
import 'package:flutter/material.dart';

class ScheduleBottomSheet extends StatefulWidget {
  const ScheduleBottomSheet({super.key});

  @override
  State<ScheduleBottomSheet> createState() => _ScheduleBottomSheetState();
}

class _ScheduleBottomSheetState extends State<ScheduleBottomSheet> {
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Container(
        height: MediaQuery.of(context).size.height / 2, // 화면에 반을 차지하는 흰색 컨테이너 틀
        color: Colors.white,
      //====================================================
        child: CustomTextField(
          label: '시작 시간',
      //====================================================
        ),
      ),
    );
  }
}

텍스트 필드 위젯 디자인하기

import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class CustomTextField extends StatelessWidget {
  final String label; // 텍스트 필드 제목
  final bool isTime; // 시간 선택하는 텍스트 필드인지 확인여부용

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

  @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 ? '시' : null, // 시간 관련 텍스트는 '시' 접미사 추가
            ),
          ),
        ),
      ],
    );
  }
}

UI 완성하기

import 'package:calendar_scheduler/component/custom_text_field.dart';
import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';

class ScheduleBottomSheet extends StatefulWidget {
  const ScheduleBottomSheet({super.key});

  @override
  State<ScheduleBottomSheet> createState() => _ScheduleBottomSheetState();
}

class _ScheduleBottomSheetState extends State<ScheduleBottomSheet> {
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Container(
        height: MediaQuery.of(context).size.height / 2, // 화면에 반을 차지하는 흰색 컨테이너 틀
        color: Colors.white,
        child: Padding(
          padding: const EdgeInsets.only(left: 8, right: 8, top: 8),
          child: Column(
            children: [
              Row( // 시작시간, 종료시간 필드는 가로로 배치
                children: [
                  Expanded(
                    child: CustomTextField( // 시작 시간 입력 필드
                      label: '시작 시간',
                      isTime: true,
                    ),
                  ),
                  const SizedBox(width: 16.0),
                  Expanded(
                    child: CustomTextField( // 종료 시간 입력 필드
                      label: '종료 시간',
                      isTime: true,
                    ),
                  ),
                ],
              ),
              SizedBox(height: 8.0),
              Expanded(
                child: CustomTextField( // 내용 입력 필드
                  label: '내용',
                  isTime: false,
                ),
              ),
              SizedBox(
                width: double.infinity,
                child: ElevatedButton( // 저장 버튼
                  onPressed: onSavePressed,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: PRIMARY_COLOR,
                  ),
                  child: Text('저장'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  void onSavePressed() {}
}

키보드 높이에 따라 입력 화면 조정하기

위에 결과를 보면 키보드가 올라와서 입력창들이 가려지는걸 볼 수 있다.

키보드 크기만큼 패딩을 추가해서 키보드가 올라와도 모든 위젯들이 잘 보이도록 변경해보겠다.


.
.
.

class ScheduleBottomSheet extends StatefulWidget {
  const ScheduleBottomSheet({super.key});

  @override
  State<ScheduleBottomSheet> createState() => _ScheduleBottomSheetState();
}

class _ScheduleBottomSheetState extends State<ScheduleBottomSheet> {
  @override
  Widget build(BuildContext context) {
    //=====================================================================
    // 키보드 높이 가져오기
    final bottomInset = MediaQuery.of(context).viewInsets.bottom;

    return SafeArea(
      child: Container(
        height: MediaQuery.of(context).size.height / 2 +
            bottomInset, // 화면 반 높이에 키보드 높이 추가하기
        color: Colors.white,
        child: Padding(
          padding:
              EdgeInsets.only(left: 8, right: 8, top: 8, bottom: bottomInset), // 패딩에 키보드 높이 추가해서 위젯 올려주기
	//=================================================================================          
          child: Column(
           
           .
           .
           .
           
}

MediaQuery 의 viewInsets.를 가져오면 시스템이 차지하는 화면 아랫부분 크기를 알 수 있다.

showModalBottomSheet() 함수 조정으로 키보드 높이 조정 완료하기

.
.
.

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  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(
            // BottomSheet 열기
            context: context,
            isDismissible: true, 
            builder: (_) => ScheduleBottomSheet(),
            //=================================
            // BottomSheet 의 높이를 화면의 최대 높이로 정의하고 스크롤 가능하게 변경
            isScrollControlled: true,
            //=================================
          );
        },
        child: Icon( // '+' 모양 아이콘
          Icons.add,
        ),
      ),
      
      .
      .
      .
      
}

showModalBottomSheet 함수는 기본적으로 최대 높이를 화면의 반으로 규정한다.

isScrollControlled 매개변수에 true 를 넣어주면서 간단하게 최대 높이를 화면 전체로 변경할 수 있다.

달력 언어 설정하기

main 함수에서 intl 패키지 초기화하기

main 함수를 비동기로 변경하고 initializeDateFormatting() 으로 intl 패키지를 초기화 (다국어화) 해준다.

import 'package:calendar_scheduler/screen/home_screen.dart';
import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart';

void main() async{
  WidgetsFlutterBinding.ensureInitialized(); // 플러터 프레임워크가 준비될 때 까지 대기

  await initializeDateFormatting(); // intl 패키지 초기화 (다국어화)

  runApp(
    MaterialApp(
      home: HomeScreen(),
    ),
  );
}

TableCalendar 위젯에 한국어 적용하기

locale 매개변수에 한국어를 의미하는 'ko_KR' 을 입력해주면 된다.

class MainCalendar extends StatelessWidget {

.
.
.

@override
  Widget build(BuildContext context) {
    return TableCalendar(
      locale: 'ko_KR', // 한국어로 언어 변경
      
      .
      .
      .
}      

profile
아프지 말자 - (잘못된 정보, 수정 사항 있으면 언제든지 알려주시면 감사하겠습니다!)

0개의 댓글