From b6719c5fa233f822dd111d37683026fb9918ffb4 Mon Sep 17 00:00:00 2001
From: tpgus2603 <kakaneymar2424@gmail.com>
Date: Fri, 22 Nov 2024 20:28:13 +0900
Subject: [PATCH] =?UTF-8?q?refactor:=20=EC=8A=A4=EC=BC=80=EC=A5=B4=20?=
 =?UTF-8?q?=EB=AA=A8=EB=8D=B8,=EC=84=9C=EB=B9=84=EC=8A=A4=EC=BD=94?=
 =?UTF-8?q?=EB=93=9C=20=EB=8C=80=ED=8F=AD=EC=88=98=EC=A0=95(#13)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 controllers/meetingController.js  |   6 +-
 controllers/scheduleController.js | 116 +++++----
 dtos/FriendListDTO.js             |   5 +-
 dtos/FriendResponseDTO.js         |  32 ++-
 dtos/ScheduleRequestDTO.js        |  44 ++--
 dtos/ScheduleResponseDTO.js       |   6 +-
 models/schedule.js                |  34 +--
 services/friendService.test.js    |   1 -
 services/schedule.test.js         | 407 +++++++++++++++---------------
 services/scheduleService.js       | 354 +++++++++-----------------
 10 files changed, 463 insertions(+), 542 deletions(-)

diff --git a/controllers/meetingController.js b/controllers/meetingController.js
index 182c25b..6775b59 100644
--- a/controllers/meetingController.js
+++ b/controllers/meetingController.js
@@ -10,7 +10,7 @@ class MeetingController {
      */
     async createMeeting(req, res) {
         try {
-            const userId = req.userId; // 인증 미들웨어를 통해 설정된 사용자 ID
+            const userId = req.user.id;
             const meetingData = { ...req.body, created_by: userId };
 
             // CreateMeetingRequestDTO를 사용하여 요청 데이터 검증
@@ -31,7 +31,7 @@ class MeetingController {
      */
     async getMeetings(req, res) {
         try {
-            const userId = req.userId; // 인증 미들웨어를 통해 설정된 사용자 ID
+            const userId = req.user.id; // 인증 미들웨어를 통해 설정된 사용자 ID
 
             const meetings = await MeetingService.getMeetings(userId);
             res.status(200).json(meetings);
@@ -64,7 +64,7 @@ class MeetingController {
     async joinMeeting(req, res) {
         try {
             const { meetingId } = req.params;
-            const userId = req.userId; // 인증 미들웨어를 통해 설정된 사용자 ID
+            const userId = req.user.id; // 인증 미들웨어를 통해 설정된 사용자 ID
 
             await MeetingService.joinMeeting(meetingId, userId);
             res.status(200).json({ message: '모임 및 채팅방 참가 완료' });
diff --git a/controllers/scheduleController.js b/controllers/scheduleController.js
index 0d3f1c9..4d43ee1 100644
--- a/controllers/scheduleController.js
+++ b/controllers/scheduleController.js
@@ -6,28 +6,46 @@ class scheduleController {
     /**
      * 스케줄 생성
      * POST /api/schedule
+     * 해당 사용자 id는 auth 미들웨어에서 설정된 사용자 정보 이용
+     * req.user = User 모델의 인스턴스
+     * 요청 본문 예시:
+     * {
+     *   title: 'Schedule Title',
+     *   is_fixed: true,
+     *   events: [
+     *     { time_idx: 36 },
+     *     { time_idx: 37 },
+     *     // ...
+     *   ]
+     * }
      */
     async createSchedule(req, res) {
         try {
             const userId = req.user.id;
             const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
-            const validatedData = scheduleRequestDTO.validate('create');
+            const validatedData = scheduleRequestDTO.validate('create'); // 'create' 타입 검증
 
-            const scheduleDTO = await ScheduleService.createSchedule({
+            const { title, is_fixed, events } = validatedData;
+
+            const schedules = await ScheduleService.createSchedules({
                 userId,
-                ...validatedData
+                title,
+                is_fixed,
+                events
             });
 
             return res.status(201).json({
                 success: true,
-                data: scheduleDTO
+                data: {
+                    schedules
+                }
             });
         } catch (error) {
             return res.status(400).json({
                 success: false,
                 error: {
-                    message: error.message.includes('Validation error') ? error.message : 'SCHEDULE_CREATE_ERROR',
-                    code: error.message.includes('Validation error') ? 'VALIDATION_ERROR' : 'SCHEDULE_CREATE_ERROR'
+                    message: error.message,
+                    code: 'SCHEDULE_CREATE_ERROR'
                 }
             });
         }
@@ -35,23 +53,35 @@ class scheduleController {
 
     /**
      * 스케줄 수정
-     * PUT /api/schedule/:id
+     * PUT /api/schedule
+     * Bulk update 지원
+     * 요청 본문 예시:
+     * {
+     *   updates: [
+     *     { time_idx: 36, title: 'New Title', is_fixed: true },
+     *     { time_idx: 44, title: 'Another Title' },
+     *     // ...
+     *   ]
+     * }
      */
-    async updateSchedule(req, res) {
+    async updateSchedules(req, res) {
         try {
-            const { id } = req.params;
             const userId = req.user.id;
             const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
-            const validatedData = scheduleRequestDTO.validate('update');
+            const validatedData = scheduleRequestDTO.validate('bulk_update'); // 'bulk_update' 타입 검증
+
+            const { updates } = validatedData;
 
-            const scheduleDTO = await ScheduleService.updateSchedule(id, userId, validatedData);
+            const updatedSchedules = await ScheduleService.updateSchedules(userId, updates);
 
             return res.status(200).json({
                 success: true,
-                data: scheduleDTO
+                data: {
+                    schedules: updatedSchedules
+                }
             });
         } catch (error) {
-            if (error.message === 'Schedule not found') {
+            if (error.code === 'SCHEDULE_NOT_FOUND') {
                 return res.status(404).json({
                     success: false,
                     error: {
@@ -59,14 +89,6 @@ class scheduleController {
                         code: 'SCHEDULE_NOT_FOUND'
                     }
                 });
-            } else if (error.message.includes('Validation error')) {
-                return res.status(400).json({
-                    success: false,
-                    error: {
-                        message: error.message,
-                        code: 'VALIDATION_ERROR'
-                    }
-                });
             }
             return res.status(400).json({
                 success: false,
@@ -80,34 +102,36 @@ class scheduleController {
 
     /**
      * 스케줄 삭제
-     * DELETE /api/schedule/:id
+     * DELETE /api/schedule
+     * Bulk delete 지원
+     * 요청 본문 예시:
+     * {
+     *   time_idxs: [36, 44, ...]
+     * }
      */
-    async deleteSchedule(req, res) {
+    async deleteSchedules(req, res) {
         try {
-            const { id } = req.params;
             const userId = req.user.id;
+            const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
+            const validatedData = scheduleRequestDTO.validate('bulk_delete'); // 'bulk_delete' 타입 검증
+
+            const { time_idxs } = validatedData;
 
-            const deleteResult = await ScheduleService.deleteSchedule(id, userId);
+            const result = await ScheduleService.deleteSchedules(userId, time_idxs);
 
             return res.status(200).json({
                 success: true,
-                data: deleteResult
+                data: {
+                    message: 'Schedules successfully deleted',
+                    deleted_time_idxs: result.deleted_time_idxs
+                }
             });
         } catch (error) {
-            if (error.message === 'Schedule not found') {
-                return res.status(404).json({
-                    success: false,
-                    error: {
-                        message: error.message,
-                        code: 'SCHEDULE_NOT_FOUND'
-                    }
-                });
-            }
-            return res.status(500).json({
+            return res.status(404).json({
                 success: false,
                 error: {
-                    message: 'Failed to delete schedule',
-                    code: 'DELETE_ERROR'
+                    message: error.message,
+                    code: 'SCHEDULE_DELETE_ERROR'
                 }
             });
         }
@@ -120,11 +144,11 @@ class scheduleController {
     async getAllSchedules(req, res) {
         try {
             const userId = req.user.id;
-            const schedulesDTO = await ScheduleService.getAllSchedules(userId);
+            const schedules = await ScheduleService.getAllSchedules(userId);
 
             return res.status(200).json({
                 success: true,
-                data: schedulesDTO
+                data: schedules
             });
         } catch (error) {
             return res.status(500).json({
@@ -139,17 +163,19 @@ class scheduleController {
 
     /**
      * 해당 사용자 특정 스케줄 조회
-     * GET /api/schedule/:id
+     * GET /api/schedule/:time_idx
+     * 예: GET /api/schedule/36
      */
-    async getScheduleById(req, res) {
+    async getScheduleByTimeIdx(req, res) {
         try {
-            const { id } = req.params;
+            const { time_idx } = req.params;
             const userId = req.user.id;
-            const scheduleDTO = await ScheduleService.getScheduleById(id, userId);
+
+            const schedule = await ScheduleService.getScheduleByTimeIdx(userId, parseInt(time_idx, 10));
 
             return res.status(200).json({
                 success: true,
-                data: scheduleDTO
+                data: schedule
             });
         } catch (error) {
             if (error.message === 'Schedule not found') {
diff --git a/dtos/FriendListDTO.js b/dtos/FriendListDTO.js
index 1768c74..3dd8b22 100644
--- a/dtos/FriendListDTO.js
+++ b/dtos/FriendListDTO.js
@@ -1,10 +1,7 @@
 // dto/FriendListDTO.js
 
 class FriendListDTO {
-  /**
-   * @param {object} friend - Friend relationship object retrieved from the database.
-   * @param {number} userId - The ID of the user whose friend list is being retrieved.
-   */
+ 
   constructor(friend, userId) {
       this.id = friend.id;
       this.status = friend.status;
diff --git a/dtos/FriendResponseDTO.js b/dtos/FriendResponseDTO.js
index 8a7f365..050786a 100644
--- a/dtos/FriendResponseDTO.js
+++ b/dtos/FriendResponseDTO.js
@@ -1,25 +1,23 @@
-// dto/FriendRequestDTO.js
+// dto/FriendResponseDTO.js
 
-class FriendRequestDTO {
-  /**
-   * @param {object} friendRequest - Friend request object retrieved from the database.
-   */
-  constructor(friendRequest) {
-      this.id = friendRequest.id;
+class FriendResponseDTO {
+
+  constructor(friendResponse) {
+      this.id = friendResponse.id;
       this.requester = {
-          id: friendRequest.requester.id,
-          name: friendRequest.requester.name,
-          email: friendRequest.requester.email
+          id: friendResponse.requester.id,
+          name: friendResponse.requester.name,
+          email: friendResponse.requester.email
       };
       this.receiver = {
-          id: friendRequest.receiver.id,
-          name: friendRequest.receiver.name,
-          email: friendRequest.receiver.email
+          id: friendResponse.receiver.id,
+          name: friendResponse.receiver.name,
+          email: friendResponse.receiver.email
       };
-      this.status = friendRequest.status;
-      this.createdAt = friendRequest.createdAt;
-      this.updatedAt = friendRequest.updatedAt;
+      this.status = friendResponse.status;
+      this.createdAt = friendResponse.createdAt;
+      this.updatedAt = friendResponse.updatedAt;
   }
 }
 
-module.exports = FriendRequestDTO;
+module.exports = FriendResponseDTO;
diff --git a/dtos/ScheduleRequestDTO.js b/dtos/ScheduleRequestDTO.js
index a332534..854bd3f 100644
--- a/dtos/ScheduleRequestDTO.js
+++ b/dtos/ScheduleRequestDTO.js
@@ -1,39 +1,49 @@
 // dtos/ScheduleRequestDTO.js
-
 const Joi = require('joi');
 
 class ScheduleRequestDTO {
     constructor(data) {
         this.data = data;
     }
+
     validate(type = 'create') {
-        // 기본 스키마 정의
-        let schema = Joi.object({
-            title: Joi.string().min(1).max(255).required(),
-            start_time: Joi.date().iso().required(),
-            end_time: Joi.date().iso().required(),
-            is_fixed: Joi.boolean().required()
-        });
+        let schema;
 
-        // 'update' 타입의 경우 모든 필드를 필수로 하지 않을 수 있음
-        if (type === 'update') {
+        if (type === 'create') {
+            schema = Joi.object({
+                title: Joi.string().min(1).max(255).required(),
+                is_fixed: Joi.boolean().required(),
+                events: Joi.array().items(
+                    Joi.object({
+                        time_idx: Joi.number().integer().min(0).max(671).required(),
+                    })
+                ).min(1).required()
+            });
+        } else if (type === 'bulk_update') {
+            schema = Joi.object({
+                updates: Joi.array().items(
+                    Joi.object({
+                        time_idx: Joi.number().integer().min(0).max(671).required(),
+                        title: Joi.string().min(1).max(255).optional(),
+                        is_fixed: Joi.boolean().optional(),
+                    })
+                ).min(1).required()
+            });
+        } else if (type === 'bulk_delete') {
             schema = Joi.object({
-                title: Joi.string().min(1).max(255).optional(),
-                start_time: Joi.date().iso().optional(),
-                end_time: Joi.date().iso().optional(),
-                is_fixed: Joi.boolean().optional()
-            }).or('title', 'start_time', 'end_time', 'is_fixed'); // 최소 한 개 이상의 필드가 필요
+                time_idxs: Joi.array().items(
+                    Joi.number().integer().min(0).max(671).required()
+                ).min(1).required()
+            });
         }
 
         const { error, value } = schema.validate(this.data, { abortEarly: false });
 
         if (error) {
-            // 모든 에러 메시지를 하나의 문자열로 결합
             const errorMessages = error.details.map(detail => detail.message).join(', ');
             throw new Error(`Validation error: ${errorMessages}`);
         }
 
-        // 검증된 데이터를 반환
         return value;
     }
 }
diff --git a/dtos/ScheduleResponseDTO.js b/dtos/ScheduleResponseDTO.js
index b8717cf..a75316e 100644
--- a/dtos/ScheduleResponseDTO.js
+++ b/dtos/ScheduleResponseDTO.js
@@ -3,12 +3,10 @@
 class ScheduleResponseDTO {
   constructor(schedule) {
       this.id = schedule.id;
-      this.user_id = schedule.user_id; 
+      this.user_id = schedule.user_id;
       this.title = schedule.title;
-      this.start_time = schedule.start_time;
-      this.end_time = schedule.end_time;
+      this.time_idx = schedule.time_idx; // 새로운 time_idx 필드 추가
       this.is_fixed = schedule.is_fixed;
-      this.expiry_date = schedule.expiry_date;
       this.createdAt = schedule.createdAt;
       this.updatedAt = schedule.updatedAt;
   }
diff --git a/models/schedule.js b/models/schedule.js
index 92aca80..a7c4a42 100644
--- a/models/schedule.js
+++ b/models/schedule.js
@@ -8,29 +8,33 @@ const Schedule = sequelize.define('Schedule', {
     type: DataTypes.STRING,
     allowNull: false,
   },
-  start_time: {
-    type: DataTypes.DATE,
-    allowNull: false,
-  },
-  end_time: {
-    type: DataTypes.DATE,
+  time_idx: { // 일주일을 15분 단위로 나눈 시간 인덱스
+    type: DataTypes.INTEGER,
     allowNull: false,
+    validate: {
+      min: 0,
+      max: 671, // 7일 * 24시간 * 4 (15분 단위) - 1
+    },
   },
   is_fixed: {
     type: DataTypes.BOOLEAN,
     allowNull: false,
-    defaultValue: false
+    defaultValue: false,
   },
-  expiry_date: {
-    type: DataTypes.DATE,
-    allowNull: true
-  }
 }, {
   tableName: 'Schedules',
-  timestamps: true,  // created_at과 updated_at 자동 관리
+  timestamps: true, // createdAt과 updatedAt 자동 관리
+  indexes: [
+    {
+      unique: true,
+      fields: ['user_id', 'time_idx'],
+      name: 'unique_schedule_per_user_time',
+    },
+    {
+      fields: ['time_idx'],
+    },
+  ],
 });
 
-// Schedule.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
-// User.hasMany(Schedule, { foreignKey: 'user_id', as: 'schedules' });
 
-module.exports = Schedule;
\ No newline at end of file
+module.exports = Schedule;
diff --git a/services/friendService.test.js b/services/friendService.test.js
index db3e03e..02394ee 100644
--- a/services/friendService.test.js
+++ b/services/friendService.test.js
@@ -4,7 +4,6 @@ const sequelize = require('../config/sequelize'); // Sequelize 인스턴스 임
 // const User = require('../models/User');
 // const Friend = require('../models/Friend');
 const { Friend,User} = require('../models');
-
 const friendService = require('./friendService'); // FriendService 임포트
 
 // Sequelize의 Op를 가져오기 위해 추가
diff --git a/services/schedule.test.js b/services/schedule.test.js
index d1cc0f2..b5d5626 100644
--- a/services/schedule.test.js
+++ b/services/schedule.test.js
@@ -1,244 +1,237 @@
 // test/schedule.test.js
-
 const sequelize = require('../config/sequelize');
-const User = require('../models/User');
-const Friend = require('../models/Friend');
-const Schedule = require('../models/Schedule');
-const scheduleService = require('./scheduleService'); 
+const {User, Friend, Schedule,} = require('../models'); 
+const scheduleService = require('../services/scheduleService'); // 경로 수정
 
 beforeAll(async () => {
-  await sequelize.sync({ force: true });
-  // 더미 사용자 생성
-  await User.bulkCreate([
-    { id: 1, name: 'Alice', email: 'alice@example.com' },
-    { id: 2, name: 'Bob', email: 'bob@example.com' },
-  ]);
-
-  // 더미 친구 관계 생성
-  await Friend.create({
-    id: 1,
-    requester_id: 1,
-    receiver_id: 2,
-    status: 'ACCEPTED',
-  });
-
-  // 더미 스케줄 생성
-  await Schedule.create({
-    id: 1,
-    user_id: 1,
-    title: 'Alice\'s Fixed Schedule',
-    start_time: new Date('2024-05-01T09:00:00Z'),
-    end_time: new Date('2024-05-01T10:00:00Z'),
-    is_fixed: true,
-    expiry_date: null,
-  });
-
-  await Schedule.create({
-    id: 2,
-    user_id: 1,
-    title: 'Alice\'s Flexible Schedule',
-    start_time: new Date('2024-05-02T11:00:00Z'),
-    end_time: new Date('2024-05-02T12:00:00Z'),
-    is_fixed: false,
-    expiry_date: new Date('2024-05-08T00:00:00Z'), // 다음 월요일
-  });
-});
-
-afterAll(async () => {
-  // 데이터베이스 연결 종료
-  await sequelize.close();
-});
+    await sequelize.sync({ force: true });
+
+    // 더미 사용자 생성
+    await User.bulkCreate([
+        { id: 1, name: 'Alice', email: 'alice@example.com' },
+        { id: 2, name: 'Bob', email: 'bob@example.com' },
+    ]);
+
+    // 더미 친구 관계 생성
+    await Friend.create({
+        id: 1,
+        requester_id: 1,
+        receiver_id: 2,
+        status: 'ACCEPTED',
+    });
 
-describe('Schedule Service', () => {
-  describe('createSchedule', () => {
-    test('should create a new fixed schedule successfully', async () => {
-      const scheduleData = {
-        userId: 2,
-        title: 'Bob\'s Fixed Schedule',
-        start_time: new Date('2024-05-03T14:00:00Z'),
-        end_time: new Date('2024-05-03T15:00:00Z'),
+    // 더미 스케줄 생성 (day_of_week과 TIME 형식 사용)
+    await Schedule.create({
+        id: 1,
+        user_id: 1,
+        title: 'Alice\'s Fixed Schedule',
+        day_of_week: 'Monday',
+        start_time: '09:00:00', // 'HH:MM:SS' 형식
+        end_time: '10:00:00',
         is_fixed: true,
-      };
-
-      const schedule = await scheduleService.createSchedule(scheduleData);
-      
-
-      expect(schedule).toBeDefined();
-      expect(schedule.user_id).toBe(2);
-      expect(schedule.title).toBe('Bob\'s Fixed Schedule');
-      expect(schedule.is_fixed).toBe(true);
-      expect(schedule.expiry_date).toBeNull();
     });
 
-    test('should create a new flexible schedule with expiry date', async () => {
-      const scheduleData = {
-        userId: 2,
-        title: 'Bob\'s Flexible Schedule',
-        start_time: new Date('2024-05-04T16:00:00Z'),
-        end_time: new Date('2024-05-04T17:00:00Z'),
+    await Schedule.create({
+        id: 2,
+        user_id: 1,
+        title: 'Alice\'s Flexible Schedule',
+        day_of_week: 'Tuesday',
+        start_time: '11:00:00',
+        end_time: '12:00:00',
         is_fixed: false,
-      };
+    });
+});
 
-      const schedule = await scheduleService.createSchedule(scheduleData);
+afterAll(async () => {
+    // 데이터베이스 연결 종료
+    await sequelize.close();
+});
 
-      expect(schedule).toBeDefined();
-      expect(schedule.user_id).toBe(2);
-      expect(schedule.title).toBe('Bob\'s Flexible Schedule');
-      expect(schedule.is_fixed).toBe(false);
-      expect(schedule.expiry_date).toBeInstanceOf(Date);
+describe('Schedule Service', () => {
+    describe('createSchedules', () => {
+        test('should create new fixed schedules successfully', async () => {
+            const scheduleData = {
+                userId: 2,
+                title: 'Bob\'s Fixed Schedule',
+                is_fixed: true,
+                events: [
+                    {
+                        day_of_week: 'Wednesday',
+                        start_time: '14:00:00',
+                        end_time: '15:00:00',
+                    },
+                ],
+            };
+
+            const schedules = await scheduleService.createSchedules(scheduleData);
+
+            expect(schedules).toBeDefined();
+            expect(Array.isArray(schedules)).toBe(true);
+            expect(schedules.length).toBe(1);
+
+            const schedule = schedules[0];
+            expect(schedule.user_id).toBe(2);
+            expect(schedule.title).toBe('Bob\'s Fixed Schedule');
+            expect(schedule.is_fixed).toBe(true);
+            expect(schedule.day_of_week).toBe('Wednesday');
+            expect(schedule.start_time).toBe('14:00');
+            expect(schedule.end_time).toBe('15:00');
+        });
 
-      // expiry_date가 다음 월요일로 설정되었는지 확인
-      const expectedExpiryDate = new Date('2024-05-06T00:00:00Z'); // 2024-05-06은 다음 월요일
-      expect(schedule.expiry_date.toISOString()).toBe(expectedExpiryDate.toISOString());
-    });
+        test('should create new flexible schedules successfully', async () => {
+            const scheduleData = {
+                userId: 2,
+                title: 'Bob\'s Flexible Schedule',
+                is_fixed: false,
+                events: [
+                    {
+                        day_of_week: 'Thursday',
+                        start_time: '16:00:00',
+                        end_time: '17:00:00',
+                    },
+                ],
+            };
+
+            const schedules = await scheduleService.createSchedules(scheduleData);
+
+            expect(schedules).toBeDefined();
+            expect(Array.isArray(schedules)).toBe(true);
+            expect(schedules.length).toBe(1);
+
+            const schedule = schedules[0];
+            expect(schedule.user_id).toBe(2);
+            expect(schedule.title).toBe('Bob\'s Flexible Schedule');
+            expect(schedule.is_fixed).toBe(false);
+            expect(schedule.day_of_week).toBe('Thursday');
+            expect(schedule.start_time).toBe('16:00');
+            expect(schedule.end_time).toBe('17:00');
+        });
 
-    test('should throw error when schedule times overlap with existing schedule', async () => {
-      const scheduleData = {
-        userId: 1,
-        title: 'Alice\'s Overlapping Schedule',
-        start_time: new Date('2024-05-01T09:30:00Z'), // 기존 스케줄과 겹침
-        end_time: new Date('2024-05-01T10:30:00Z'),
-        is_fixed: false,
-      };
+        test('should throw error when schedule times overlap with existing schedule', async () => {
+            const scheduleData = {
+                userId: 1,
+                title: 'Alice\'s Overlapping Schedule',
+                is_fixed: false,
+                events: [
+                    {
+                        day_of_week: 'Monday', // 기존 스케줄과 동일한 요일
+                        start_time: '09:30:00', // 기존 스케줄과 겹치는 시간
+                        end_time: '10:30:00',
+                    },
+                ],
+            };
+
+            await expect(scheduleService.createSchedules(scheduleData))
+                .rejects
+                .toThrow('Schedule overlaps with existing schedule on Monday');
+        });
 
-      await expect(scheduleService.createSchedule(scheduleData)).rejects.toThrow('Schedule overlaps with existing schedule');
+        test('should throw error when start_time is after end_time', async () => {
+            const scheduleData = {
+                userId: 1,
+                title: 'Invalid Schedule',
+                is_fixed: false,
+                events: [
+                    {
+                        day_of_week: 'Friday',
+                        start_time: '18:00:00',
+                        end_time: '17:00:00', // start_time이 더 늦음
+                    },
+                ],
+            };
+
+            await expect(scheduleService.createSchedules(scheduleData))
+                .rejects
+                .toThrow('Start time must be before end time');
+        });
     });
 
-    test('should throw error when start_time is after end_time', async () => {
-      const scheduleData = {
-        userId: 1,
-        title: 'Invalid Schedule',
-        start_time: new Date('2024-05-05T18:00:00Z'),
-        end_time: new Date('2024-05-05T17:00:00Z'), // start_time이 더 나중
-        is_fixed: false,
-      };
+    describe('updateSchedule', () => {
+        test('should update an existing schedule successfully', async () => {
+            const updateData = {
+                title: 'Alice\'s Updated Flexible Schedule',
+                start_time: '11:30:00',
+                end_time: '12:30:00',
+            };
 
-      await expect(scheduleService.createSchedule(scheduleData)).rejects.toThrow('Start time must be before end time');
-    });
-  });
-
-  describe('updateSchedule', () => {
-    test('should update an existing schedule successfully', async () => {
-      const updateData = {
-        title: 'Alice\'s Updated Flexible Schedule',
-        start_time: new Date('2024-05-02T11:30:00Z'),
-        end_time: new Date('2024-05-02T12:30:00Z'),
-      };
-
-      const updatedSchedule = await scheduleService.updateSchedule(2, 1, updateData);
-
-      expect(updatedSchedule).toBeDefined();
-      expect(updatedSchedule.title).toBe('Alice\'s Updated Flexible Schedule');
-      expect(updatedSchedule.start_time.toISOString()).toBe(new Date('2024-05-02T11:30:00Z').toISOString());
-      expect(updatedSchedule.end_time.toISOString()).toBe(new Date('2024-05-02T12:30:00Z').toISOString());
-      expect(updatedSchedule.expiry_date).toBeInstanceOf(Date);
-    });
+            const updatedSchedule = await scheduleService.updateSchedule(2, 1, updateData);
+
+            expect(updatedSchedule).toBeDefined();
+            expect(updatedSchedule.title).toBe('Alice\'s Updated Flexible Schedule');
+            expect(updatedSchedule.start_time).toBe('11:30');
+            expect(updatedSchedule.end_time).toBe('12:30');
+        });
 
-    test('should throw error when updating a non-existing schedule', async () => {
-      const updateData = {
-        title: 'Non-existing Schedule',
-        start_time: new Date('2024-05-06T10:00:00Z'),
-        end_time: new Date('2024-05-06T11:00:00Z'),
-      };
+        test('should throw error when updating a non-existing schedule', async () => {
+            const updateData = {
+                title: 'Non-existing Schedule',
+                start_time: '10:00:00',
+                end_time: '11:00:00',
+            };
 
-      await expect(scheduleService.updateSchedule(999, 1, updateData)).rejects.toThrow('Schedule not found');
-    });
+            await expect(scheduleService.updateSchedule(999, 1, updateData))
+                .rejects
+                .toThrow('Schedule not found');
+        });
 
-    test('should throw error when updated schedule overlaps with existing schedule', async () => {
-      const updateData = {
-        title: 'Alice\'s Overlapping Update',
-        start_time: new Date('2024-05-01T09:30:00Z'), // 기존 스케줄과 겹침
-        end_time: new Date('2024-05-01T10:30:00Z'),
-      };
+        test('should throw error when updated schedule overlaps with existing schedule', async () => {
+            const updateData = {
+                title: 'Alice\'s Overlapping Update',
+                start_time: '09:30:00', // 기존 스케줄과 겹침
+                end_time: '10:30:00',
+            };
 
-      await expect(scheduleService.updateSchedule(2, 1, updateData)).rejects.toThrow('Schedule overlaps with existing schedule');
+            await expect(scheduleService.updateSchedule(2, 1, updateData))
+                .rejects
+                .toThrow('Schedule overlaps with existing schedule');
+        });
     });
-  });
 
-  describe('deleteSchedule', () => {
-    test('should delete an existing schedule successfully', async () => {
-        const result = await scheduleService.deleteSchedule(2, 1);
+    describe('deleteSchedule', () => {
+        test('should delete an existing schedule successfully', async () => {
+            const result = await scheduleService.deleteSchedule(2, 1);
 
-        expect(result).toEqual({ message: 'Schedule successfully deleted' });
+            expect(result).toEqual({ message: 'Schedule successfully deleted' });
 
-        // 삭제된 스케줄이 실제로 삭제되었는지 확인
-        const schedule = await Schedule.findByPk(2);
-        expect(schedule).toBeNull();
-    });
+            // 삭제된 스케줄이 실제로 삭제되었는지 확인
+            const schedule = await Schedule.findByPk(2);
+            expect(schedule).toBeNull();
+        });
 
-    test('should throw error when deleting a non-existing schedule', async () => {
-        await expect(scheduleService.deleteSchedule(999, 1)).rejects.toThrow('Schedule not found');
+        test('should throw error when deleting a non-existing schedule', async () => {
+            await expect(scheduleService.deleteSchedule(999, 1))
+                .rejects
+                .toThrow('Schedule not found');
+        });
     });
-});
 
-  describe('getAllSchedules', () => {
-    test('should retrieve all valid schedules for a user', async () => {
-      // 사용자 Alice의 모든 스케줄 조회
-      const schedules = await scheduleService.getAllSchedules(1);
+    describe('getAllSchedules', () => {
+        test('should retrieve all valid schedules for a user', async () => {
+            // 사용자 Alice의 모든 스케줄 조회 (user_id: 1)
+            const schedules = await scheduleService.getAllSchedules(1);
 
-      expect(schedules.length).toBe(1); // id=1 스케줄은 is_fixed=true
-      expect(schedules[0].title).toBe('Alice\'s Fixed Schedule');
+            expect(schedules).toBeDefined();
+            expect(Array.isArray(schedules)).toBe(true);
+            expect(schedules.length).toBe(1); // id=1 스케줄은 is_fixed=true
+            expect(schedules[0].title).toBe('Alice\'s Fixed Schedule');
+            expect(schedules[0].day_of_week).toBe('Monday');
+        });
     });
-  });
-
-  describe('getScheduleById', () => {
-    test('should retrieve a specific schedule by ID', async () => {
-      const schedule = await scheduleService.getScheduleById(1, 1);
 
-      expect(schedule).toBeDefined();
-      expect(schedule.title).toBe('Alice\'s Fixed Schedule');
-    });
+    describe('getScheduleById', () => {
+        test('should retrieve a specific schedule by ID', async () => {
+            const schedule = await scheduleService.getScheduleById(1, 1);
 
-    test('should throw error when retrieving a non-existing schedule', async () => {
-      await expect(scheduleService.getScheduleById(999, 1)).rejects.toThrow('Schedule not found');
-    });
-  });
-  // test/schedule.test.js
-
-  describe('cleanExpiredSchedules', () => {
-    test('should delete expired flexible schedules', async () => {
-        // 현재 날짜를 기준으로 만료된 스케줄과 만료되지 않은 스케줄 생성
-        const now = new Date('2024-05-07T00:00:00Z'); // 테스트를 위한 고정된 현재 날짜
-
-        // Jest의 Fake Timers를 사용하여 Date를 고정
-        jest.useFakeTimers('modern');
-        jest.setSystemTime(now);
-
-        // 만료된 유동 스케줄 생성
-        await Schedule.create({
-            user_id: 1,
-            title: 'Expired Flexible Schedule',
-            start_time: new Date('2024-04-25T10:00:00Z'),
-            end_time: new Date('2024-04-25T11:00:00Z'),
-            is_fixed: false,
-            expiry_date: new Date('2024-05-06T00:00:00Z'), // 이미 만료됨
+            expect(schedule).toBeDefined();
+            expect(schedule.title).toBe('Alice\'s Fixed Schedule');
+            expect(schedule.day_of_week).toBe('Monday');
         });
 
-        // 만료되지 않은 유동 스케줄 생성
-        await Schedule.create({
-            user_id: 1,
-            title: 'Valid Flexible Schedule',
-            start_time: new Date('2024-05-07T10:00:00Z'),
-            end_time: new Date('2024-05-07T11:00:00Z'),
-            is_fixed: false,
-            expiry_date: new Date('2024-05-14T00:00:00Z'), // 아직 만료되지 않음
+        test('should throw error when retrieving a non-existing schedule', async () => {
+            await expect(scheduleService.getScheduleById(999, 1))
+                .rejects
+                .toThrow('Schedule not found');
         });
-
-        // 만료된 스케줄 정리
-        await scheduleService.cleanExpiredSchedules();
-
-        // 만료된 스케줄이 삭제되었는지 확인
-        const expiredSchedule = await Schedule.findOne({ where: { title: 'Expired Flexible Schedule' } });
-        expect(expiredSchedule).toBeNull();
-
-        // 만료되지 않은 스케줄은 남아있는지 확인
-        const validSchedule = await Schedule.findOne({ where: { title: 'Valid Flexible Schedule' } });
-        expect(validSchedule).toBeDefined();
-        expect(validSchedule.title).toBe('Valid Flexible Schedule');
-
-        // Jest의 Fake Timers를 복구
-        jest.useRealTimers();
     });
-});
-  
+
 });
diff --git a/services/scheduleService.js b/services/scheduleService.js
index e7c5a3b..419b49a 100644
--- a/services/scheduleService.js
+++ b/services/scheduleService.js
@@ -1,261 +1,157 @@
 // services/scheduleService.js
 const sequelize = require('../config/sequelize');
 const { Op } = require('sequelize');
-const {Schedule} = require('../models');
+const Schedule = require('../models/Schedule');
 const ScheduleResponseDTO = require('../dtos/ScheduleResponseDTO');
 
-class scheduleService {
-  /**
-   * 트랜잭션 래퍼 함수
-   */
-  async withTransaction(callback) {
-        const transaction = await sequelize.transaction(); // 직접 sequelize 사용
-        try {
-            const result = await callback(transaction);
-            await transaction.commit();
-            return result;
-        } catch (error) {
-            await transaction.rollback();
-            throw error;
-        }
-    }
-
-  /**
-   * 공통 where 절 생성
-   */
-  getScheduleWhereClause(userId, id = null) {
-    const where = {
-      user_id: userId,
-      [Op.or]: [
-        { is_fixed: true },
-        {
-          is_fixed: false,
-          expiry_date: { [Op.gt]: new Date() },
-        },
-      ],
-    };
-
-    if (id) {
-      where.id = id;
+class ScheduleService {
+    /**
+     * 스케줄 생성 (벌크)
+     */
+    async createSchedules({ userId, title, is_fixed, events }) {
+        return await sequelize.transaction(async (transaction) => {
+            const scheduleDTOs = [];
+
+            for (const event of events) {
+                const { time_idx } = event;
+
+                // 중복 스케줄 검사
+                const overlap = await this.checkScheduleOverlap(
+                    userId,
+                    time_idx,
+                    transaction
+                );
+
+                if (overlap) {
+                    throw new Error(`Schedule overlaps with existing schedule at time_idx ${time_idx}`);
+                }
+
+                const scheduleData = {
+                    user_id: userId,
+                    title,
+                    time_idx,
+                    is_fixed,
+                };
+
+                const schedule = await Schedule.create(scheduleData, { transaction });
+                scheduleDTOs.push(new ScheduleResponseDTO(schedule));
+            }
+
+            return scheduleDTOs;
+        });
     }
 
-    return where;
-  }
-
-  /**
-   * 스케줄 유효성 검사
-   * 이미 컨트롤러에서 검증했으므로, 추가 검증 필요 시 수행
-   */
-  validateScheduleTime(start_time, end_time) {
-    if (new Date(start_time) >= new Date(end_time)) {
-      throw new Error("Start time must be before end time");
-    }
-  }
-
-  /**
-   * 유동 스케줄 만료일 구하기
-   */
-  getNextMonday(startTime) {
-    const date = new Date(startTime);
-    const day = date.getUTCDay(); // 0=Sunday, 1=Monday, ..., 6=Saturday
-    const daysUntilNextMonday = (8 - day) % 7 || 7; // Ensure next Monday
-
-    const nextMonday = new Date(
-      Date.UTC(
-        date.getUTCFullYear(),
-        date.getUTCMonth(),
-        date.getUTCDate() + daysUntilNextMonday,
-        0,
-        0,
-        0,
-        0 // Set to midnight UTC
-      )
-    );
-
-    return nextMonday;
-  }
-
-  /**
-   * 사용자 스케줄 생성
-   */
-  async createSchedule({ userId, title, start_time, end_time, is_fixed }) {
-    const schedule = await this.withTransaction(async (transaction) => {
-      this.validateScheduleTime(start_time, end_time);
+    /**
+     * 스케줄 수정 (벌크)
+     */
+    async updateSchedules(userId, updates) {
+        return await sequelize.transaction(async (transaction) => {
+            const updatedSchedules = [];
 
-      const overlap = await this.checkScheduleOverlap(
-        userId,
-        start_time,
-        end_time
-      );
-      if (overlap) {
-        throw new Error("Schedule overlaps with existing schedule");
-      }
+            for (const update of updates) {
+                const { time_idx, title, is_fixed } = update;
 
-      const scheduleData = {
-        user_id: userId,
-        title,
-        start_time,
-        end_time,
-        is_fixed,
-        expiry_date: is_fixed ? null : this.getNextMonday(start_time),
-      };
+                const schedule = await Schedule.findOne({
+                    where: { user_id: userId, time_idx },
+                    transaction,
+                });
 
-      return Schedule.create(scheduleData, { transaction });
-    });
+                if (!schedule) {
+                    throw { code: 'SCHEDULE_NOT_FOUND', message: `Schedule not found at time_idx ${time_idx}` };
+                }
 
-    return new ScheduleResponseDTO(schedule);
-  }
+                // 중복 스케줄 검사 (time_idx는 고유하므로 필요 없음)
+                // 만약 다른 필드를 기반으로 중복을 검사한다면 추가 로직 필요
 
-  /**
-   * 사용자 스케줄 수정
-   */
-  async updateSchedule(id, userId, updateData) {
-    const updatedSchedule = await this.withTransaction(async (transaction) => {
-      const schedule = await Schedule.findOne({
-        where: { id, user_id: userId },
-        transaction,
-      });
+                const updatedData = {};
+                if (title !== undefined) updatedData.title = title;
+                if (is_fixed !== undefined) updatedData.is_fixed = is_fixed;
 
-      if (!schedule) {
-        throw new Error("Schedule not found");
-      }
+                const updatedSchedule = await schedule.update(updatedData, { transaction });
+                updatedSchedules.push(new ScheduleResponseDTO(updatedSchedule));
+            }
 
-      // 이미 컨트롤러에서 검증했으므로, 추가 검증이 필요하다면 수행
-      if (updateData.start_time && updateData.end_time) {
-        this.validateScheduleTime(updateData.start_time, updateData.end_time);
-      }
-
-      const overlap = await this.checkScheduleOverlap(
-        userId,
-        updateData.start_time || schedule.start_time,
-        updateData.end_time || schedule.end_time,
-        id
-      );
-      if (overlap) {
-        throw new Error("Schedule overlaps with existing schedule");
-      }
-
-      const is_fixed = schedule.is_fixed;
-      const updatedDataWithExpiry = {
-        ...updateData,
-        expiry_date: is_fixed
-          ? null
-          : this.getNextMonday(updateData.start_time || schedule.start_time),
-        updatedAt: new Date(),
-      };
-      delete updatedDataWithExpiry.is_fixed;
-
-      return schedule.update(updatedDataWithExpiry, { transaction });
-    });
+            return updatedSchedules;
+        });
+    }
 
-    return new ScheduleResponseDTO(updatedSchedule);
-  }
+    /**
+     * 스케줄 삭제 (벌크)
+     */
+    async deleteSchedules(userId, time_idxs) {
+        return await sequelize.transaction(async (transaction) => {
+            const deleted_time_idxs = [];
 
-  /**
-   * 사용자 스케줄 삭제
-   */
-  async deleteSchedule(id, userId) {
-    return this.withTransaction(async (transaction) => {
-      const result = await Schedule.destroy({
-        where: { id, user_id: userId },
-        transaction,
-      });
+            for (const time_idx of time_idxs) {
+                const deletedCount = await Schedule.destroy({
+                    where: { user_id: userId, time_idx },
+                    transaction,
+                });
 
-      if (!result) {
-        throw new Error("Schedule not found");
-      }
+                if (deletedCount === 0) {
+                    throw new Error(`Schedule not found at time_idx ${time_idx}`);
+                }
 
-      // 삭제 성공 메시지 반환
-      return { message: "Schedule successfully deleted" };
-    });
-  }
+                deleted_time_idxs.push(time_idx);
+            }
 
-  /**
-   * 해당 사용자의 스케줄 정보 조회
-   */
-  async getAllSchedules(userId) {
-    try {
-      const schedules = await Schedule.findAll({
-        where: this.getScheduleWhereClause(userId),
-        order: [["start_time", "ASC"]],
-      });
-      const schedulesDTO = schedules.map(
-        (schedule) => new ScheduleResponseDTO(schedule)
-      );
-      return schedulesDTO;
-    } catch (error) {
-      throw new Error(`Failed to fetch schedules: ${error.message}`);
+            return { deleted_time_idxs };
+        });
     }
-  }
 
-  /**
-   * 해당 사용자의 특정 스케줄 조회
-   */
-  async getScheduleById(id, userId) {
-    try {
-      const schedule = await Schedule.findOne({
-        where: this.getScheduleWhereClause(userId, id),
-      });
+    /**
+     * 특정 time_idx로 스케줄 조회
+     */
+    async getScheduleByTimeIdx(userId, time_idx) {
+        const schedule = await Schedule.findOne({
+            where: { user_id: userId, time_idx },
+        });
 
-      if (!schedule) {
-        throw new Error("Schedule not found");
-      }
+        if (!schedule) {
+            throw new Error('Schedule not found');
+        }
 
-      return new ScheduleResponseDTO(schedule);
-    } catch (error) {
-      throw new Error(`Failed to fetch schedule: ${error.message}`);
+        return new ScheduleResponseDTO(schedule);
     }
-  }
 
-  /**
-   * 만료된 유동 스케줄 정리
-   */
-  async cleanExpiredSchedules() {
-    try {
-      await Schedule.destroy({
-        where: {
-          is_fixed: false,
-          expiry_date: { [Op.lte]: new Date() },
-        },
-      });
-    } catch (error) {
-      throw new Error(`Failed to clean expired schedules: ${error.message}`);
+    /**
+     * 모든 스케줄 조회
+     */
+    async getAllSchedules(userId) {
+        try {
+            const schedules = await Schedule.findAll({
+                where: { user_id: userId },
+                order: [['time_idx', 'ASC']],
+            });
+            return schedules.map((schedule) => new ScheduleResponseDTO(schedule));
+        } catch (error) {
+            throw new Error(`Failed to fetch schedules: ${error.message}`);
+        }
     }
-  }
 
-  /**
-   * 스케줄 중복 검사
-   */
-  async checkScheduleOverlap(userId, start_time, end_time, excludeId = null) {
-    try {
-      const where = {
-        user_id: userId,
-        [Op.or]: [
-          {
-            [Op.and]: [
-              { start_time: { [Op.lte]: start_time } },
-              { end_time: { [Op.gte]: start_time } },
-            ],
-          },
-          {
-            [Op.and]: [
-              { start_time: { [Op.gte]: start_time } },
-              { start_time: { [Op.lte]: end_time } },
-            ],
-          },
-        ],
-      };
+    /**
+     * 중복 스케줄 검사
+     */
+    async checkScheduleOverlap(userId, time_idx, transaction) {
+        const overlappingSchedule = await Schedule.findOne({
+            where: { user_id: userId, time_idx },
+            transaction,
+        });
 
-      if (excludeId) {
-        where.id = { [Op.ne]: excludeId };
-      }
+        return !!overlappingSchedule;
+    }
 
-      const overlappingSchedule = await Schedule.findOne({ where });
-      return overlappingSchedule;
-    } catch (error) {
-      throw new Error(`Failed to check schedule overlap: ${error.message}`);
+    async cleanExpiredSchedules() {
+        try {
+            const deletedCount = await Schedule.destroy({
+                where: { is_fixed: false },
+            });
+            //console.log(`Deleted ${deletedCount} flexible schedules.`);
+        } catch (error) {
+            console.error('Failed to clean expired schedules:', error);
+            throw error;
+        }
     }
-  }
 }
 
-module.exports = new scheduleService();
+module.exports = new ScheduleService();
-- 
GitLab