Skip to content
Snippets Groups Projects
Commit 4b8d1508 authored by tpgus2603's avatar tpgus2603
Browse files

Merge branch 'develop' into deploy

parents 01c9fd42 b00d1dc8
Branches
No related tags found
1 merge request!42[#25] 배포코드 master브랜치로 이동
...@@ -23,21 +23,17 @@ class scheduleController { ...@@ -23,21 +23,17 @@ class scheduleController {
try { try {
const userId = req.user.id; const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO(req.body); const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
const validatedData = scheduleRequestDTO.validate('create'); // 'create' 타입 검증 const validatedData = scheduleRequestDTO.validate('create');
const { title, is_fixed, events } = validatedData; const schedule = await ScheduleService.createSchedules({
const schedules = await ScheduleService.createSchedules({
userId, userId,
title, ...validatedData
is_fixed,
events
}); });
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
data: { data: {
schedules schedule
} }
}); });
} catch (error) { } catch (error) {
...@@ -57,31 +53,28 @@ class scheduleController { ...@@ -57,31 +53,28 @@ class scheduleController {
* Bulk update 지원 * Bulk update 지원
* 요청 본문 예시: * 요청 본문 예시:
* { * {
* updates: [ * "originalTitle": "알고리즘 스터디", // 기존 스케줄의 제목
* { time_idx: 36, title: 'New Title', is_fixed: true }, * "title": "알고리즘 스터디 2.0", // 변경할 제목 (제목 변경 안할거면 기존 제목을 넣어야함 * -> title로 동일 스케줄을 찾아서)
* { time_idx: 44, title: 'Another Title' }, * "is_fixed": true,
* // ... * "time_indices": [36, 37, 38, 40] // 변경할 time_indices 배열
* ]
* } * }
*/ */
async updateSchedules(req, res) { async updateSchedules(req, res) {
try { try {
const userId = req.user.id; const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO(req.body); const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
const validatedData = scheduleRequestDTO.validate('bulk_update'); // 'bulk_update' 타입 검증 const validatedData = scheduleRequestDTO.validate('bulk_update');
const { updates } = validatedData; const updatedSchedule = await ScheduleService.updateSchedules(userId, validatedData);
const updatedSchedules = await ScheduleService.updateSchedules(userId, updates);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: { data: {
schedules: updatedSchedules schedule: updatedSchedule
} }
}); });
} catch (error) { } catch (error) {
if (error.code === 'SCHEDULE_NOT_FOUND') { if (error.message === 'Schedule not found') {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
error: { error: {
...@@ -106,24 +99,21 @@ class scheduleController { ...@@ -106,24 +99,21 @@ class scheduleController {
* Bulk delete 지원 * Bulk delete 지원
* 요청 본문 예시: * 요청 본문 예시:
* { * {
* time_idxs: [36, 44, ...] * "title": "알고리즘 스터디"
* } * }
*/ */
async deleteSchedules(req, res) { async deleteSchedules(req, res) {
try { try {
const userId = req.user.id; const userId = req.user.id;
const scheduleRequestDTO = new ScheduleRequestDTO(req.body); const scheduleRequestDTO = new ScheduleRequestDTO(req.body);
const validatedData = scheduleRequestDTO.validate('bulk_delete'); // 'bulk_delete' 타입 검증 const validatedData = scheduleRequestDTO.validate('bulk_delete');
const result = await ScheduleService.deleteSchedules(userId, validatedData.title);
const { time_idxs } = validatedData;
const result = await ScheduleService.deleteSchedules(userId, time_idxs);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: { data: {
message: 'Schedules successfully deleted', message: 'Schedule successfully deleted',
deleted_time_idxs: result.deleted_time_idxs deletedCount: result.deletedCount
} }
}); });
} catch (error) { } catch (error) {
...@@ -136,7 +126,6 @@ class scheduleController { ...@@ -136,7 +126,6 @@ class scheduleController {
}); });
} }
} }
/** /**
* 해당 사용자 전체 스케줄 조회 * 해당 사용자 전체 스케줄 조회
* GET /api/schedule/all * GET /api/schedule/all
...@@ -148,7 +137,9 @@ class scheduleController { ...@@ -148,7 +137,9 @@ class scheduleController {
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: schedules data: {
schedules
}
}); });
} catch (error) { } catch (error) {
return res.status(500).json({ return res.status(500).json({
...@@ -171,11 +162,15 @@ class scheduleController { ...@@ -171,11 +162,15 @@ class scheduleController {
const { time_idx } = req.params; const { time_idx } = req.params;
const userId = req.user.id; const userId = req.user.id;
const schedule = await ScheduleService.getScheduleByTimeIdx(userId, parseInt(time_idx, 10)); const scheduleRequestDTO = new ScheduleRequestDTO({ time_idx: parseInt(time_idx, 10) });
const validatedData = scheduleRequestDTO.validate('get_by_time_idx');
const schedule = await ScheduleService.getScheduleByTimeIdx(userId, validatedData.time_idx);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
data: schedule data: {
schedule
}
}); });
} catch (error) { } catch (error) {
if (error.message === 'Schedule not found') { if (error.message === 'Schedule not found') {
......
...@@ -13,27 +13,28 @@ class ScheduleRequestDTO { ...@@ -13,27 +13,28 @@ class ScheduleRequestDTO {
schema = Joi.object({ schema = Joi.object({
title: Joi.string().min(1).max(255).required(), title: Joi.string().min(1).max(255).required(),
is_fixed: Joi.boolean().required(), is_fixed: Joi.boolean().required(),
events: Joi.array().items( time_indices: Joi.array()
Joi.object({ .items(Joi.number().integer().min(0).max(671))
time_idx: Joi.number().integer().min(0).max(671).required(), .min(1)
}) .required()
).min(1).required()
}); });
} else if (type === 'bulk_update') { } else if (type === 'bulk_update') {
schema = Joi.object({ schema = Joi.object({
updates: Joi.array().items( originalTitle: Joi.string().min(1).max(255).required(),
Joi.object({ title: Joi.string().min(1).max(255).required(),
time_idx: Joi.number().integer().min(0).max(671).required(), is_fixed: Joi.boolean().required(),
title: Joi.string().min(1).max(255).optional(), time_indices: Joi.array()
is_fixed: Joi.boolean().optional(), .items(Joi.number().integer().min(0).max(671))
}) .min(1)
).min(1).required() .required()
}); });
} else if (type === 'bulk_delete') { } else if (type === 'bulk_delete') {
schema = Joi.object({ schema = Joi.object({
time_idxs: Joi.array().items( title: Joi.string().min(1).max(255).required()
Joi.number().integer().min(0).max(671).required() });
).min(1).required() } else if (type === 'get_by_time_idx') {
schema = Joi.object({
time_idx: Joi.number().integer().min(0).max(671).required()
}); });
} }
......
// dtos/ScheduleResponseDTO.js // dtos/ScheduleResponseDTO.js
class ScheduleResponseDTO { class ScheduleResponseDTO {
constructor(schedule) { static groupSchedules(schedules) {
this.id = schedule.id; const grouped = schedules.reduce((acc, schedule) => {
this.user_id = schedule.user_id; const key = `${schedule.title}-${schedule.is_fixed}`;
this.title = schedule.title; if (!acc[key]) {
this.time_idx = schedule.time_idx; // 새로운 time_idx 필드 추가 acc[key] = {
this.is_fixed = schedule.is_fixed; id: schedule.id,
this.createdAt = schedule.createdAt; user_id: schedule.user_id,
this.updatedAt = schedule.updatedAt; title: schedule.title,
is_fixed: schedule.is_fixed,
time_indices: [],
createdAt: schedule.createdAt,
updatedAt: schedule.updatedAt
};
}
acc[key].time_indices.push(schedule.time_idx);
return acc;
}, {});
return Object.values(grouped);
} }
} }
......
...@@ -118,20 +118,16 @@ class MeetingService { ...@@ -118,20 +118,16 @@ class MeetingService {
{ transaction } { transaction }
); );
// 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성) const time_indices = Array.from(
const events = []; { length: time_idx_end - time_idx_start + 1 },
for (let idx = time_idx_start; idx <= time_idx_end; idx++) { (_, i) => time_idx_start + i
events.push({ time_idx: idx }); );
} await ScheduleService.createSchedules({
await ScheduleService.createSchedules(
{
userId: created_by, userId: created_by,
title: `번개 모임: ${title}`, title: `번개 모임: ${title}`,
is_fixed: false, is_fixed: false,
events: events, time_indices: time_indices,
}, }, transaction);
transaction
);
// 친구 초대 로직 호출 // 친구 초대 로직 호출
const invitedFriendIds = await this.sendInvites({ const invitedFriendIds = await this.sendInvites({
...@@ -264,24 +260,17 @@ class MeetingService { ...@@ -264,24 +260,17 @@ class MeetingService {
{ transaction } { transaction }
); );
// 스케줄 생성 (모임 시간 범위 내 모든 time_idx에 대해 생성) const time_indices = Array.from(
const events = []; { length: meeting.time_idx_end - meeting.time_idx_start + 1 },
for ( (_, i) => meeting.time_idx_start + i
let idx = meeting.time_idx_start; );
idx <= meeting.time_idx_end;
idx++ await ScheduleService.createSchedules({
) {
events.push({ time_idx: idx });
}
await ScheduleService.createSchedules(
{
userId: userId, userId: userId,
title: `번개 모임: ${meeting.title}`, title: `번개 모임: ${meeting.title}`,
is_fixed: false, is_fixed: false,
events: events, time_indices: time_indices,
}, }, transaction);
transaction
);
// 채팅방 참가 (MongoDB) // 채팅방 참가 (MongoDB)
const user = await User.findOne({ const user = await User.findOne({
......
// test/scheduleService.test.js
const sequelize = require('../config/sequelize');
const { Schedule, User, Meeting, MeetingParticipant, FcmToken } = require('../models');
const ScheduleService = require('../services/scheduleService');
const MeetingService = require('../services/meetingService');
const ChatRooms = require('../schemas/chatRooms');
describe('Schedule Service and Meeting Integration Tests', () => {
beforeAll(async () => {
await sequelize.sync({ force: true });
});
beforeEach(async () => {
await MeetingParticipant.destroy({ where: {} });
await Meeting.destroy({ where: {} });
await Schedule.destroy({ where: {} });
await User.destroy({ where: {} });
await FcmToken.destroy({ where: {} });
// 더미 사용자 생성
await User.bulkCreate([
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' }
]);
// ChatRooms Mock 설정
jest.spyOn(ChatRooms.prototype, 'save').mockResolvedValue(undefined);
jest.spyOn(ChatRooms, 'findOne').mockResolvedValue({
participants: [],
isOnline: new Map(),
lastReadAt: new Map(),
lastReadLogId: new Map(),
save: jest.fn().mockResolvedValue(true)
});
});
afterAll(async () => {
await sequelize.close();
});
describe('Schedule Service Tests', () => {
test('should create schedule with time_indices', async () => {
const scheduleData = {
userId: 1,
title: 'Test Schedule',
is_fixed: true,
time_indices: [36, 37, 38]
};
const schedule = await ScheduleService.createSchedules(scheduleData);
expect(schedule).toBeDefined();
expect(schedule.title).toBe('Test Schedule');
expect(schedule.time_indices).toEqual([36, 37, 38]);
const dbSchedules = await Schedule.findAll({
where: { user_id: 1, title: 'Test Schedule' }
});
expect(dbSchedules.length).toBe(3);
});
test('should update schedule with new time_indices', async () => {
await ScheduleService.createSchedules({
userId: 1,
title: 'Original Schedule',
is_fixed: true,
time_indices: [36, 37, 38]
});
const updateData = {
originalTitle: 'Original Schedule',
title: 'Updated Schedule',
is_fixed: true,
time_indices: [36, 37, 38, 39]
};
const updatedSchedule = await ScheduleService.updateSchedules(1, updateData);
expect(updatedSchedule.title).toBe('Updated Schedule');
expect(updatedSchedule.time_indices).toEqual([36, 37, 38, 39]);
});
test('should delete schedule by title', async () => {
await ScheduleService.createSchedules({
userId: 1,
title: 'Schedule to Delete',
is_fixed: true,
time_indices: [40, 41, 42]
});
const result = await ScheduleService.deleteSchedules(1, 'Schedule to Delete');
expect(result.deletedCount).toBe(3);
const remainingSchedules = await Schedule.findAll({
where: { user_id: 1, title: 'Schedule to Delete' }
});
expect(remainingSchedules.length).toBe(0);
});
});
describe('Meeting Integration Tests', () => {
beforeEach(() => {
jest.spyOn(User, 'findOne').mockResolvedValue({
id: 1,
name: 'Alice',
email: 'alice@example.com',
fcmTokenList: []
});
});
test('should create meeting with correct schedules', async () => {
const meetingData = {
title: 'Test Meeting',
time_idx_start: 50,
time_idx_end: 52,
created_by: 1,
type: 'OPEN',
max_num: 5
};
const meeting = await MeetingService.createMeeting(meetingData);
const creatorSchedules = await Schedule.findAll({
where: {
user_id: 1,
title: `번개 모임: ${meetingData.title}`
}
});
expect(creatorSchedules.length).toBe(3);
expect(creatorSchedules.map(s => s.time_idx).sort()).toEqual([50, 51, 52]);
});
test('should create correct schedules when joining meeting', async () => {
const meetingData = {
title: 'Join Test Meeting',
time_idx_start: 60,
time_idx_end: 62,
created_by: 1,
type: 'OPEN',
max_num: 5,
time_idx_deadline: 59
};
const meeting = await MeetingService.createMeeting(meetingData);
jest.spyOn(MeetingService, 'getCurrentTimeIdx').mockReturnValue(58);
await MeetingService.joinMeeting(meeting.meeting_id, 2);
const participantSchedules = await Schedule.findAll({
where: {
user_id: 2,
title: `번개 모임: ${meetingData.title}`
}
});
expect(participantSchedules.length).toBe(3);
expect(participantSchedules.map(s => s.time_idx).sort()).toEqual([60, 61, 62]);
});
test('should handle schedule conflicts correctly', async () => {
await ScheduleService.createSchedules({
userId: 2,
title: 'Existing Schedule',
is_fixed: true,
time_indices: [70, 71]
});
const meetingData = {
title: 'Conflict Test Meeting',
time_idx_start: 70,
time_idx_end: 72,
created_by: 1,
type: 'OPEN',
max_num: 5,
time_idx_deadline: 69
};
const meeting = await MeetingService.createMeeting(meetingData);
await expect(
MeetingService.joinMeeting(meeting.meeting_id, 2)
).rejects.toThrow('스케줄이 겹칩니다');
});
});
});
\ No newline at end of file
...@@ -9,158 +9,222 @@ class ScheduleService { ...@@ -9,158 +9,222 @@ class ScheduleService {
* 스케줄 생성 (벌크) * 스케줄 생성 (벌크)
* @param {object} [transaction] - Sequelize 트랜잭션 객체 -> 미팅방에서 쓰기위해 트랜잭션을 넘겨받는걸 추가 * @param {object} [transaction] - Sequelize 트랜잭션 객체 -> 미팅방에서 쓰기위해 트랜잭션을 넘겨받는걸 추가
*/ */
async createSchedules({ userId, title, is_fixed, events }, transaction = null) { async createSchedules({ userId, title, is_fixed, time_indices }, transaction = null) {
const scheduleDTOs = []; // 중복 검사
for (const time_idx of time_indices) {
for (const event of events) {
const { time_idx } = event;
// 중복 스케줄 검사
const overlap = await this.checkScheduleOverlap(userId, time_idx, transaction); const overlap = await this.checkScheduleOverlap(userId, time_idx, transaction);
if (overlap) { if (overlap) {
throw new Error(`Schedule overlaps with existing schedule at time_idx ${time_idx}`); throw new Error(`Schedule overlaps at time_idx ${time_idx}`);
}
} }
const scheduleData = { const createdSchedules = await Promise.all(
time_indices.map(time_idx =>
Schedule.create({
user_id: userId, user_id: userId,
title, title,
time_idx, time_idx,
is_fixed
}, { transaction })
)
);
return {
id: createdSchedules[0].id,
user_id: userId,
title,
is_fixed, is_fixed,
time_indices,
createdAt: createdSchedules[0].createdAt,
updatedAt: createdSchedules[0].updatedAt
}; };
const schedule = await Schedule.create(scheduleData, { transaction });
scheduleDTOs.push(new ScheduleResponseDTO(schedule));
} }
return scheduleDTOs; async getAllSchedules(userId) {
try {
const schedules = await Schedule.findAll({
where: { user_id: userId },
order: [['time_idx', 'ASC']]
});
return ScheduleResponseDTO.groupSchedules(schedules);
} catch (error) {
throw new Error(`Failed to fetch schedules: ${error.message}`);
}
} }
/**
* 스케줄 수정 (벌크)
* @param {Array} updates - 수정할 스케줄 배열
*/
async updateSchedules(userId, updates, transaction = null) { async updateSchedules(userId, updates, transaction = null) {
const updatedSchedules = []; const { originalTitle, title, is_fixed, time_indices } = updates;
for (const update of updates) { // 기존 스케줄 조회
const { time_idx, title, is_fixed } = update; const existingSchedules = await Schedule.findAll({
where: {
const schedule = await Schedule.findOne({ user_id: userId,
where: { user_id: userId, time_idx }, title: originalTitle
transaction, },
transaction
}); });
if (!schedule) { if (existingSchedules.length === 0) {
throw new Error(`Schedule not found at time_idx ${time_idx}`); throw new Error('Schedule not found');
} }
const updatedData = {}; const existingTimeIndices = existingSchedules.map(s => s.time_idx); // 기존 시간대
if (title !== undefined) updatedData.title = title; const toDelete = existingTimeIndices.filter(idx => !time_indices.includes(idx)); // 삭제할 시간대
if (is_fixed !== undefined) updatedData.is_fixed = is_fixed; const toAdd = time_indices.filter(idx => !existingTimeIndices.includes(idx)); // 추가할 시간대
const t = transaction || await sequelize.transaction();
const updatedSchedule = await schedule.update(updatedData, { transaction }); try {
updatedSchedules.push(new ScheduleResponseDTO(updatedSchedule)); // 삭제
if (toDelete.length > 0) {
await Schedule.destroy({
where: {
user_id: userId,
title: originalTitle,
time_idx: {
[Op.in]: toDelete
} }
},
return updatedSchedules; transaction: t
});
} }
/** // 제목, 고정/유동 업데이트
* 스케줄 삭제 (벌크) await Schedule.update(
* @param {number} userId - 사용자 ID {
* @param {Array<number>} time_idxs - 삭제할 스케줄의 time_idx 배열 title,
* @param {object} [transaction] - Sequelize 트랜잭션 객체 is_fixed
*/ },
async deleteSchedules(userId, time_idxs, transaction = null) { {
const deleted_time_idxs = []; where: {
user_id: userId,
title: originalTitle
},
transaction: t
}
);
for (const time_idx of time_idxs) { // 새로운 time_indices 추가
const deletedCount = await Schedule.destroy({ if (toAdd.length > 0) {
where: { user_id: userId, time_idx }, await Promise.all(
transaction, toAdd.map(time_idx =>
}); Schedule.create({
user_id: userId,
title,
time_idx,
is_fixed
}, { transaction: t })
)
);
}
if (deletedCount === 0) { if (!transaction) {
throw new Error(`Schedule not found at time_idx ${time_idx}`); await t.commit();
} }
deleted_time_idxs.push(time_idx); return {
id: existingSchedules[0].id,
user_id: userId,
title,
is_fixed,
time_indices,
createdAt: existingSchedules[0].createdAt,
updatedAt: new Date()
};
} catch (error) {
if (!transaction) {
await t.rollback();
}
throw error;
}
} }
return { deleted_time_idxs }; async deleteSchedules(userId, title, transaction = null) {
const deletedSchedules = await Schedule.destroy({
where: {
user_id: userId,
title
},
transaction
});
return { deletedCount: deletedSchedules };
} }
/** /**
* 특정 time_idx로 스케줄 조회 * 특정 time_idx로 스케줄 조회
*/ */
async getScheduleByTimeIdx(userId, time_idx) { async getScheduleByTimeIdx(userId, time_idx) {
// 해당 time_idx의 스케줄 찾기
const schedule = await Schedule.findOne({ const schedule = await Schedule.findOne({
where: { user_id: userId, time_idx }, where: { user_id: userId, time_idx }
}); });
if (!schedule) { if (!schedule) {
throw new Error('Schedule not found'); throw new Error('Schedule not found');
} }
return new ScheduleResponseDTO(schedule); // 같은 제목의 모든 스케줄 찾기
const relatedSchedules = await Schedule.findAll({
where: {
user_id: userId,
title: schedule.title,
is_fixed: schedule.is_fixed
},
order: [['time_idx', 'ASC']]
});
return ScheduleResponseDTO.groupSchedules(relatedSchedules)[0];
} }
/**
* 모든 스케줄 조회
*/
async getAllSchedules(userId) { async getAllSchedules(userId) {
try { try {
const schedules = await Schedule.findAll({ const schedules = await Schedule.findAll({
where: { user_id: userId }, where: { user_id: userId },
order: [['time_idx', 'ASC']], order: [['time_idx', 'ASC']]
}); });
return schedules.map((schedule) => new ScheduleResponseDTO(schedule)); return ScheduleResponseDTO.groupSchedules(schedules);
} catch (error) { } catch (error) {
throw new Error(`Failed to fetch schedules: ${error.message}`); throw new Error(`Failed to fetch schedules: ${error.message}`);
} }
} }
/**
* 중복 스케줄 검사
*/
async checkScheduleOverlap(userId, time_idx, transaction = null) { async checkScheduleOverlap(userId, time_idx, transaction = null) {
const overlappingSchedule = await Schedule.findOne({ const overlappingSchedule = await Schedule.findOne({
where: { user_id: userId, time_idx }, where: { user_id: userId, time_idx },
transaction, transaction
}); });
return !!overlappingSchedule; return !!overlappingSchedule;
} }
async checkScheduleOverlapByTime(userId, time_idx_start, time_idx_end, transaction = null) { async checkScheduleOverlapByTime(userId, time_idx_start, time_idx_end, transaction = null) {
console.log( const overlappingSchedules = await Schedule.findAll({
`checkScheduleOverlapByTime 호출: userId=${userId}, time_idx_start=${time_idx_start}, time_idx_end=${time_idx_end}`
);
const overlappingSchedule = await Schedule.findOne({
where: { where: {
user_id: userId, user_id: userId,
time_idx: { time_idx: {
[Op.between]: [time_idx_start, time_idx_end] [Op.between]: [time_idx_start, time_idx_end]
} }
}, },
transaction, transaction
}); });
console.log(`중복 스케줄: ${JSON.stringify(overlappingSchedule)}`);
const result = !!overlappingSchedule; const groupedSchedules = ScheduleResponseDTO.groupSchedules(overlappingSchedules);
const result = groupedSchedules.length > 0;
console.log(`checkScheduleOverlapByTime 호출: userId=${userId}, time_idx_start=${time_idx_start}, time_idx_end=${time_idx_end}`);
console.log(`중복 스케줄: ${JSON.stringify(groupedSchedules)}`);
console.log(`스케줄 충돌 결과: ${result}`); console.log(`스케줄 충돌 결과: ${result}`);
return result; return result;
} }
/**
* 만료된 스케줄 삭제
*/
async cleanExpiredSchedules() { async cleanExpiredSchedules() {
try { try {
const deletedCount = await Schedule.destroy({ const deletedCount = await Schedule.destroy({
where: { is_fixed: false }, where: { is_fixed: false }
}); });
//console.log(`Deleted ${deletedCount} flexible schedules.`); return { deletedCount };
} catch (error) { } catch (error) {
console.error('Failed to clean expired schedules:', error); console.error('Failed to clean expired schedules:', error);
throw error; throw error;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment