[Cleaning Gym] 제작기 근데 AI를 곁들인
![[Cleaning Gym] 제작기 근데 AI를 곁들인 글의 썸네일 이미지](/_next/image?url=https%3A%2F%2Fvelog.velcdn.com%2Fimages%2Fd159123%2Fpost%2F8b8979a6-3492-433d-bee2-d71d23a090ba%2Fimage.png&w=1920&q=75)
🤔 시작하게된 동기
나는 어렸을 때부터 부모님 따라 배드민턴 치는 것을 좋아했고 지금도 주 4회 이상 배드민턴을 즐겨칠 정도로 배드민턴에 미친 사람이다. 집 근처 체육관에 있는 배드민턴 클럽에 가입한 지 2년이 되어가는데, 매년 집행부와 이사진을 변경해나가며 클럽 운영을 이끌어나가고 있다.
클럽을 어떻게 운영하든 운동만 할 수 있다면 아무 신경도 쓰지 않았지만 같이 운동하는 친한 형이 집행부에 들어가게 되면서 우연찮게 내부 운영 방식을 엿볼 수 있게 되었다.
체육관은 매일 저녁마다 운영되며 주말을 제외한 평일에는 청소 당번들이 마무리를 짓고 체육관 문을 닫는다. 집행부에서는 매달 청소 당번 리스트를 엑셀로 뽑아서 단체 톡방에 공지하였었는데, 그동안 수동으로 일마다 인원을 직접 선정해서 달력표를 만들어 왔던 것이다.

이번에 친한 형이 청소표 작성 인수인계를 받으면서, 랜덤으로 청소표를 만들 수 있는 엑셀 프로그램을 만든 후 나에게 틀린 부분이 있는지 검토를 부탁해주셨는데 청소표를 자동으로 생성해줄 수 있는 프로그램을 만들어보면 재밌을 것 같아서 프로젝트를 시작하게 되었다.
⚙️ 환경
작은 것부터 시작해서 점차 기능을 추가해나가는 것이 이상적이라 생각하여 프로젝트의 크기를 최소한으로 설정했다.
프레임워크: Vue 3
디자인: Vuetify
빌드 도구: Vite
배포: Vercel
이전 회사에서 Vue 2를 사용했던 경험이 있어서 이를 사용해서 이번 기회에 Vue 3도 공부해볼 겸 선택하였다.
Vuetify는 Vue Component 프레임워크로 UI 컴포넌트를 제공해주는 Vue 공식 라이브러리다.
Vite는 Vue의 창시자인 에반 유(Evan You)가 개발한 빌드 도구로 Vue의 기본 빌드 도구이다.
Vercel은 Git Repository에 있는 프로젝트를 가장 간단하게 연동해서 빠르게 배포할 수 있기에 선택하였다.
어쩌다보니 V4로 프로젝트 환경을 세팅하게 되었다.
📕 기획
목표
체육관 청소 당번을 자동으로 랜덤하게 배정하는 시스템을 제공
기능
- 회원 관리
- 샘플 엑셀 파일 제공
- 엑셀 첨부로 리스트 생성
- 회원 수동 생성
- 회원 리스트 엑셀 다운로드 기능 제공
- 청소당번 스케쥴 생성
- 해당 달의 달력 UI 생성
- 달력 내부 랜덤한 인원 배치
- (추가) 인원 Drag & Drop으로 수동 배치
- 청소표 엑셀 다운로드
- (추가) 엑셀 CSS 적용
- 청소표 이미지로 다운로드
- 청소표 공유 링크 제공
규칙
- 청소는 월,화,수,목 진행하며 금,토,일은 모두 다 같이 청소를 한다.
- 병가를 제외한 모든 회원은 월 1회 이상 청소 당번을 수행한다.
- '월/수 레슨' 혹은 '화/목 레슨'을 받는 회원은 레슨 받는 요일 중 하루를 청소 당번으로 배정한다.
- 회원은 최소 30명 이상이라고 가정한다. (하루에 최소 한 명은 청소할 수 있다.)
🤖 AI
프론트엔드에서 프론트 빼고 다 할 줄 아는 사람으로서 AI에게 전적으로 의지하면서 작업을 진행하였다.
이게 바이브 코딩인가? 싶으면서도 기능 구현은 내가 다 하고 싶다는 욕심이 있었기에 바이브 코딩을 찍먹해보는 수준으로 작업을 진행하였다.
디자인 & 퍼블리싱
먼저 머리 속으로는 어떻게 페이지를 구성할 지 흐릿하게 잡혀있지만 디자인과 퍼블리싱을 할 줄 모르기에 AI를 통해 웹 페이지의 전체적인 틀을 잡았다.
Lovable은 코딩없이 자연어를 통해서 웹 페이지를 작업하고 퍼블리싱까지 할 수 있는 AI 도구이다. 코드 작성 뿐만 아니라 직접 프롬프트르 작성하면서 Preview를 제공하기에 실시간으로 페이지를 확인하면서 수정해나갈 수 있다는 장점이 있다.

하지만 현재는 React에 대한 작업만 코드 프리뷰를 제공하고 Vue에 대해서는 프리뷰가 불가하고 코드 텍스트로만 제공해준다. 가끔은 vue로 만들어달라고 하면 거절을 당하기도 한다.

Stitch는 최근 Google 2025 I/O에서 발표된 UI 디자인 도구이다. 웹 페이지를 생성해주는 것이 아닌 UI 디자인을 생성해주고 코드 혹은 피그마로 연결시켜주는 기능을 제공한다.

Stitch는 Beta 버전이라 그런가, 프롬프트에 대한 이해도가 조금 떨어지기도 하고 아직 학습이 덜 된 것 같은 느낌이 들었다. 예를 들어, 내가 원했던 UI는 큰 캘린더에 각 일자마다 회원명이 Chip 형태로 들어가 있는 디자인이었는데, 계속에서 일자를 선택만 할 수 있는 캘린더 컴포넌트를 생성해주었다. 물론 성격이 급한 탓에 프롬프트를 대충 작성하기도 하였지만 서너 번 설명해도 디자인이 변경되지는 않았다.

결론적으로 Lovable은 놀라울 정도로 이쁜 UI와 웹 페이지 preview까지 제공하지만 React만 작업이 가능하고 Stitch는 일반적인 디자인 UI와 HTML 코드를 제공하였다.
둘 다 내가 원하는 작업을 온전히 진행할 수가 없었기에, Lovable과 stitch를 통해 원하는 디자인이 나올 때까지 돌려보고 괜찮은 디자인이 나오면 해당 코드를 Claude에게 던져서 Vue 코드로 변환하게끔 작업하였다. 어찌보면 AI 돌려막기,,
Claude는 확실히 ChatGPT보다 똑똑하다. 프롬프트 기억력도 더 길고 ChatGPT보다 더 정확한 정보들이 제공되는 것 같다. 하지만 무료가 끝나면 사용을 못하기 때문에 정말 중요할 때만 아껴쓰고 나머지는 ChatGPT로 해결하고 있다,,
로고
이미지 제공은 ChatGPT를 통해 작업하였다. 이미지 생성이나 만만한 작업은 역시 국밥 ChatGPT

라이브러리
Vuetify Vuetify 에서는 다양한 UI Vue Component들을 제공하는데, 사용해본 것빼고는 사용해본 적이 없기 때문에 적시에 필요한 최적의 컴포넌트를 찾는 것에 불편함이 있었다. 그럴 땐 주로 ChatGPT 통해서 Vuetify에서 제공하는 컴포넌트를 찾아오거나 해당 컴포넌트가 제공하는 API를 적절하게 배치하도록 코드를 제공받았다.
FullCalendar 청소표를 제작하기 위해 달력 컴포넌트를 제작하여야 했는데, Vuetify에서 제공하는
VCalendar나 다른 vue 라이브러리들은 내가 원하는 기능들을 제공하지 않아 마음에 들지 않았다. 직접 그리드로 작업해야 하나 고민하던 중에 AI통해 FullCalendar 라이브러리를 발견하게 되었다.
그래서 해당 라이브러리를 확인해보니 원하는 기능들은 전부 제공하기도 하였고 나름 NPM에서 Weekly Downloads가 10만은 꾸준히 넘어가고 있어서 사용하기로 결정하였다.
공식 페이지에서는 다행히도 데모 페이지나 샘플 코드들을 제공해주고 있어서 Claude를 통해 vue 코드로 변환시켜주었다. 샘플 코드는 해당 컴포넌트를 표현하는데 최적화된 코드고 AI는 이미 이전 프롬프트를 통해 나의 프로젝트 내용을 인지하고 있기 때문에 캘린더 컴포넌트를 내가 원하는 방식으로 샘플 데이터와 함께 코드를 작성할 수 있었다.

다만 나는 아직 해당 라이브러리에 대해 무지하기 때문에 불필요한 코드가 있는지 혹은 제대로 작성되었는 지 몰라, ChatGPT로 한 번 다시 돌려서 서로의 코드에 대해 리뷰를 하게끔 만든 후에 프로젝트에 붙여넣었다.

🔨 제작
바이브 코딩을 원했지만 기능은 내가 구현하고 싶었고 AI를 통해 제공받은 레이아웃들 중에 온전히 마음에 드는 것들이 없어서 부분 부분 필요한 것들만 빼와가면서 직접 제작하였다. 아마도 반 바이브 코딩,,
크게 컴포넌트를 나누자면 헤더, 회원 관리, 청소 스케쥴로 구분지을 수 있을 것 같다.
헤더
헤더는 Stitch에서 나온 디자인을 참고하여 제작하였다. Stitch에서 뽑은 디자인의 HTML 코드를 Claude를 통해 Vue로 변환시켰고 로고에 필요한 이미지는 ChatGPT를 통해 생성하였다.

로고, 프로그램명, 네비게이션, 사용자 아이콘으로 구성되어 있다.
아직 네비게이션과 사용자 아이콘을 아무런 기능이 존재하지 않는 상태이지만 만약 프로그램 활용도가 높아지면서 추가적인 기능들이 생겨난다면 그 때 작업이 진행될 것 같다.
아마 지금은 체육관 청소표 제작 페이지이지만 활용도가 높아져서 클럽 운영을 위한 페이지가 된다면 그 땐 로그인이나 운영을 위한 다른 페이지들이 만들어질 것 같다.
회원 관리
회원 관리 페이지는 Lovable을 통해 제작된 웹 페이지 템플릿을 참고하여 작업하였다. 이 또한 작성된 React 코드들 중에 필요한 부분만 Claude를 통해 Vue로 변환하는 작업을 거쳤다.

회원 관리 페이지에서의 전체적인 흐름은 다음과 같다.
- 사용자는 회원 추가 버튼을 통해 수동으로 회원을 추가할 수 있다.
- 제공되는 엑셀 양식을 다운받고 해당 양식에 대용량의 회원을 추가하여 업로드하면 회원 리스트에 한 번에 추가할 수 있다.
- 추가된 회원은 수정과 삭제가 가능하다
- 변경되는 회원 리스트에 대해 전체적인 통계 수치를 제공하고 회원 리스트를 엑셀로 다운받을 수 있다.
1. 통계 통계 컴포넌트는 추가, 수정, 삭제되는 회원 리스트에 따라 '총 회원수', '월/수 레슨 회원', '화/목 레슨 회원'의 총 수를 제공한다.
100명 가까이 되는 회원 수를 추가하고 리스트 내에서 작업을 하다보면 전체적인 회원의 수나 레슨을 받는 회원의 수가 올바르게 추가되었는지 검토하기 위해 사용될 수 있다.
2. 버튼 그룹 회원을 추가할 수 있는 방법은 '회원 추가' 버튼을 통한 수동 입력 방식과 엑셀을 통해 한 번에 회원을 추가할 수 있는 자동 입력 방식이 제공된다.
엑셀은 ExcelJS 라이브러리를 통해 API를 연동하여 업로드되는 엑셀 데이터를 읽거나 데이터를 엑셀 파일로 쓸 수 있는 기능을 추가하였다.
더 유명한 xlsx 라이브러리가 나에게는 더 친숙하였지만 엑셀 스타일을 작업할 수 있는 CSS 스타일링을 제공하지 않는다. 이후에 청소표를 스타일링된 엑셀 파일로 다운받을 수 있는 기능을 추가할 계획이 있기에 CSS 기능을 제공하는 ExcelJS를 선택하게 되었다.
3. 회원 리스트 및 dialog 회원 리스트에서는 한 페이지에 10명의 회원 목록이 제공되고 pagination을 통해 각 페이지에 해당하는 회원 목록을 렌더링시킨다.
회원 추가를 위한 Dialog는 기본적인 틀만 직접 작업한 뒤에 claude를 통해 디자인과 퍼블리싱을 맡겨서 작업을 진행하였다.

청소 스케쥴
기존에는 /schedule 경로를 추가하여 하나의 페이지에 Full Calender 컴포넌트를 표시할 생각이었지만 Lovable 에서 제공받은 템플릿에서 회원관리와 청소 스케쥴을 tab으로 제공하는 것이 인상깊어서 전체적인 페이지의 틀을 변경하였다.
1. FullCalendar
- 월 선택 스케쥴표를 생성하고 싶은 월을 선택하여 스케쥴을 생성할 수 있다. FullCalendar에서 제공하는 달력 헤더의 경우 필요한 것 이상으로 기능이 많기도 하고 따로 퍼블리싱이 필요할 것 같아보여, 직접 dayjs 라이브러리를 사용해 달력 해더 기능을 추가해주었다.
- 스케쥴 관리 달력의 비어있는 일자를 선택하면 회원명을 입력하여 새로운 스케쥴을 생성할 수 있고 존재하는 스케쥴을 클릭하면 삭제가 가능하다. 또한 스케쥴 Drag & Drop 기능을 활성화시켜서 스케쥴을 달력 내에서 이동시켜 편집이 가능하다.
2. Schedule Generator
주어진 규칙에 맞게 스케쥴표를 제작하는 기능이다. 이번 프로젝트에서 가장 중요하고 가장 흥미로웠던 작업이었다. 최근 알고리즘 문제를 매일 하나씩 풀면서 '어떻게 하면 더 효율적으로 문제를 풀 수 있을까?'에 대해 고민을 계속 해왔던 요즘, 이번 작업을 진행하면서 주어진 문제를 내가 푸는 것이 아닌 직접 문제를 만들고 풀어보는 과정이라 생각해서 더욱 인상깊었던 경험이었다.
우선 알고리즘을 짜기 이전에 해당 기능이 수행해야 할 전체적인 규칙을 정리해보았다.
⚠️ 규칙
- 설정된 월의 스케쥴을 작성한다.
- 청소는 월, 화, 수, 목 만 수행한다.
- 모든 회원은 월 1회만 청소를 수행한다. (회원 수는 항상 30명 이상이라 가정한다.)
- 월/수 레슨 회원은 월 혹은 수요일에 청소를 수행한다.
- 화/목 레슨 회원은 화 혹은 목요일에 청소를 수행한다.
- 레슨을 모두 받는 회원은 월, 화, 수, 목 중 상대적으로 인원이 적은 요일에 배치된다.
- 레슨을 받지 않는 회원은 비어있는 요일에 채워진다.
규칙을 작성하다보니 기능의 크기가 점점 커지는 것 같아, 작업을 세분화 시켜주었다.
✅ 실행 계획
- 해당 달의 [월/수], [화/목], [나머지] 청소 날짜를 구한다.
- [레슨X], [모든 레슨], [레슨MW], [레슨TT]로 회원 구분
- [레슨MW], [레슨TT] 회원 분배 (월/수, 화/목)
- [모든 레슨] 분배 (월,화,수,목 중 비어있는 곳으로)
- 레슨X 회원들을 비어있는 곳에 분배
❗️ 세부 계획
1. 해당 달의 [월/수], [화/목], [나머지] 청소 날짜를 구한다.
청소를 해야하는 날짜는 현재 '월, 화, 수, 목'으로 정해져있지만 추후 수정될 수도 있으니 따로 상수로 선언해주었다.
const CLEANING_DAYS = [1, 2, 3, 4];
dayjs를 이용해서 선택한 달의 모든 일자들을 가져오고 반복문을 통해 청소를 해야하는 일자들(월, 화, 수, 목)을 비교하여 [월/수], [화/목], [나머지]로 그룹을 정해주었다.
[나머지] 그룹은 사실 빈배열을 받게 되는데, 추후 청소를 해야하는 날짜가 추가될 경우를 고려하여 그룹을 만들어주었다.
2. [레슨X], [모든 레슨], [레슨MW], [레슨TT]로 회원 구분
최종 편집된 회원 리스트 또한 레슨 유무와 레슨 유형에 따라 그룹을 정해주었다.
3. [레슨MW], [레슨TT] 회원 분배 (월/수, 화/목)
1번에서 그룹을 정한 일자들과 2번에서 그룹을 정한 회원들을 매칭해서 스케쥴표를 생성해주기 시작한다.
레슨을 받는 회원들을 각걱 레슨을 받는 요일에 청소를 진행해줘야 하기 때문에 우선적으로 분배를 시켜주었다.
분배 시켜주는 방식은 '순차적' 혹은 '랜덤'으로 분배시켜줄 수 있는데, 매번 랜덤으로 스케쥴을 작성하면 매 월마다 스케쥴표를 보고 자신의 청소일자를 외워야하는 불편함이 생길 수 있다고 생각하여, 항상 자신이 청소하는 날이 고정일처럼 존재하는 것이 좋을 것 같다고 생각하여 순차적으로 분배 시키는 방식을 선택하였다.
distributeLessonMembers(members: Member[], availableDates: string[]) { members.forEach((member, index) => { const dateIndex = index % availableDates.length; const assignedDate = availableDates[dateIndex];const assignedMembers = this.dateAssignments.get(assignedDate) || []; assignedMembers.push(member); this.dateAssignments.set(assignedDate, assignedMembers); const schedule = this.initMemberSchedule(member); schedule.start = assignedDate; this.schedules.push(schedule); });
}
dateAssginments는 각 청소일자마다 배정된 인원의 비율을 비교하기 위해 선언한 Map 형식의 변수이다.
initMemberSchedule()은 매개변수로 받는 각각의 회원데이터를 스케쥴표로 생성해주는 함수이다.
schedules는 최종 캘린더에 표시하기 위해 만들어진 스케쥴 배열이다.
각 회원 그룹과 청소 그룹을 매개변수로 받아서 각 멤버마다 일자를 순차적으로 배정받고 스케쥴 데이터로 생성하여 배열에 담아주었다.
4. [모든 레슨] 분배 (월,화,수,목 중 비어있는 곳으로)
모든 레슨을 받는 회원은 월,화,수,목 중에 청소 인원이 적은 곳부터 순차적으로 배치해주었다.
distributeNonLessonMembers(members: Member[], availableDates: string[]) { members.forEach((member) => { const leastAssignedDate = this.findLeastAssignedDate(availableDates);const assignedMembers = this.dateAssignments.get(leastAssignedDate) || []; assignedMembers.push(member); this.dateAssignments.set(leastAssignedDate, assignedMembers); const schedule = this.initMemberSchedule(member); schedule.start = leastAssignedDate; this.schedules.push(schedule); });
}
레슨 멤버들을 배치해주는 메서드와 비슷하긴 하지만 findLeastAssignedDate() 함수를 통해 인원이 적은 스케쥴에 우선적으로 배치해줄 수 있게끔 작업해주었다.
findLeastAssignedDate(dates: string[]): string {
return dates.reduce((leastDate, currentDate) => {
const leastCount = this.dateAssignments.get(leastDate)?.length || 0;
const currentCount = this.dateAssignments.get(currentDate)?.length || 0;
return currentCount < leastCount ? currentDate : leastDate;
});
}
findLeastAssignedDate 에서는 미리 청소 일자와 청소 인원들을 저장하고 있는 dateAssignments을 통해 할당하고자 하는 일자의 인원 수를 조회하고 가장 적은 인원을 담고 있는 일자들을 우선적으로 반환하여 모든 레슨을 받는 회원들을 월, 화, 수, 목에 배치해주었다.
5. 레슨X 회원들을 비어있는 곳에 분배
레슨을 받지 않는 회원들 또한 distributeNonLessonMembers() 함수를 통해 동일하게 배치해주었다.
매개변수로 받는 availableDates 중에 인원이 적게 배치된 일자에 먼저 배정되는 기능이다보니 매개변수만 다르게 동일한 함수에 작업해주면 올바르게 스케쥴 표에 배치될 수 있다.
리팩토링 기존에는 하나의 알고리즘이자 유틸 함수로 생각하여 하나의 함수로만 작업을 하였는데, 생각보다 기능이 많아지고 여러 함수로 나눠지다보니 전역에서 관리해야하는 데이터들도 많아져서 리팩토링을 진행하게 되었다.
//리팩토링 이전 작업export function generateMonthlySchedule({ members, currentDate, }: GenerateMonthlyScheduleParams): MemberSchedule[] { const schedules: MemberSchedule[] = [];
//1. 해당 달의 [월/수], [화/목] [나머지] 청소 날짜를 구한다. const { cleaningDates, lessonMTDates, lessonTTDates } = getCleaningDates( currentDate, CLEANING_DAYS, );
//2. [레슨X], [모든 레슨], [레슨MW], [레슨TT]로 회원 구분 const { nonLessonMembers, allLessonMembers, lessonMWMembers, lessonTTMembers } = getSeparatedMembers(members);
// 3. [레슨MW], [레슨TT] 회원 분배 (월/수, 화/목) distributeLessonMembers(lessonMWMembers, lessonMTDates, schedules); distributeLessonMembers(lessonTTMembers, lessonTTDates, schedules);
// 4. [모든 레슨] 분배 (월,화,수,목 중 비어있는 곳으로) const allCleaningDates = [...lessonMTDates, ...lessonTTDates]; distributeNonLessonMembers(allLessonMembers, allCleaningDates, schedules);
// 5. 레슨X 회원들을 비어있는 곳에 분배 distributeNonLessonMembers(nonLessonMembers, allCleaningDates, schedules);
return schedules; }
리팩토링을 작업하기 이전에는, 각 함수에 매개변수를 전달하고 반환된 값을 또 다음 함수에 전달하면서 코드의 길이도 길어지고 가독성도 떨어진다고 생각하였다.
특히 전역에서 관리해줄 수 있는 데이터 schedules나 dateAssignments와 같은 경우에는 매개변수로 전달하지 않고 관리하는 것이 더 편리하다고 생각하였다.
또한 스케쥴을 생성할 때 각 스케쥴의 식별자를 위해 eventGuid를 생성해주어야 하는데, 이 또한 각 기능에서 작업하면 중복되는 식별자가 생길 수도 있기 때문에 하나의 클래스에서 관리해주면 이전까지 생성된 식별자의 번호를 유지할 수 있다고 생각하여 유틸함수를 ScheduleGenerator라는 클래스로 작업해주기로 결정하였다.
export class ScheduleGenerator { schedules: MemberSchedule[];eventGuid: number; dateAssignments: Map<string, Member[]>;
cleaningDates: string[]; lessonMTDates: string[]; lessonTTDates: string[];
nonLessonMembers: Member[]; allLessonMembers: Member[]; lessonMWMembers: Member[]; lessonTTMembers: Member[];
constructor() { this.schedules = [];
this.eventGuid = 0; this.dateAssignments = new Map<string, Member[]>(); this.cleaningDates = []; this.lessonMTDates = []; this.lessonTTDates = []; this.nonLessonMembers = []; this.allLessonMembers = []; this.lessonMWMembers = []; this.lessonTTMembers = [];}
generateMonthlySchedule({ members, currentDate }: GenerateMonthlyScheduleParams) { const schedules: MemberSchedule[] = [];
//1. 해당 달의 [월/수], [화/목] [나머지] 청소 날짜를 구한다. this.generateCleaningSchedules(currentDate, CLEANING_DAYS); //2. [레슨X], [모든 레슨], [레슨MW], [레슨TT]로 회원 구분 this.getSeparatedMembers(members); // 3. [레슨MW], [레슨TT] 회원 분배 (월/수, 화/목) this.distributeLessonMembers(this.lessonMWMembers, this.lessonMTDates); this.distributeLessonMembers(this.lessonTTMembers, this.lessonTTDates); // 4. [모든 레슨] 분배 (월,화,수,목 중 비어있는 곳으로) this.distributeNonLessonMembers(this.allLessonMembers, [ ...this.lessonMTDates, ...this.lessonTTDates, ]); // 5. 레슨X 회원들을 비어있는 곳에 분배 this.distributeNonLessonMembers(this.nonLessonMembers, [ ...this.lessonMTDates, ...this.lessonTTDates, ...this.cleaningDates, ]);
} ... }
클래스를 잘 사용하지는 않아, 익숙하진 않았지만 작업을 하다보니 '아 이래서 클래스를 통해 각각의 역할을 분배하는 건가?' 라는 생각이 들기도 하였고 이처럼 작업하니 코드의 가독성이나 객체의 역할을 확실히 분리해준 것 같아서 인상깊은 경험이었다.
아직 해당 클래스도 너무 많은 프로퍼티를 관리하고 있다는 생각이 있어서 이 또한 하위의 여러 클래스로 구분지어주면 좋을 것 같다는 아쉬움이 있다. DateSchedule과 MemberSchedule로 객체를 따로 구분지어 조금 더 각 객체의 책임을 분리시킬 수 있을 것 같다.
Github: gym-cleaning-rotation Web: Cleaning Gym // schedule test: schedule