Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
683d463a9f
|
|
@ -53,13 +53,20 @@ export class BookingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureDataDirectory(): void {
|
private ensureDataDirectory(): void {
|
||||||
if (!fs.existsSync(BOOKING_DIR)) {
|
try {
|
||||||
fs.mkdirSync(BOOKING_DIR, { recursive: true });
|
if (!fs.existsSync(BOOKING_DIR)) {
|
||||||
logger.info(`📁 예약 데이터 디렉토리 생성: ${BOOKING_DIR}`);
|
fs.mkdirSync(BOOKING_DIR, { recursive: true, mode: 0o755 });
|
||||||
}
|
logger.info(`📁 예약 데이터 디렉토리 생성: ${BOOKING_DIR}`);
|
||||||
if (!fs.existsSync(BOOKING_FILE)) {
|
}
|
||||||
fs.writeFileSync(BOOKING_FILE, JSON.stringify([], null, 2));
|
if (!fs.existsSync(BOOKING_FILE)) {
|
||||||
logger.info(`📄 예약 파일 생성: ${BOOKING_FILE}`);
|
fs.writeFileSync(BOOKING_FILE, JSON.stringify([], null, 2), {
|
||||||
|
mode: 0o644,
|
||||||
|
});
|
||||||
|
logger.info(`📄 예약 파일 생성: ${BOOKING_FILE}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ 예약 디렉토리 생성 실패: ${BOOKING_DIR}`, error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,13 +118,16 @@ export class BookingService {
|
||||||
priority?: string;
|
priority?: string;
|
||||||
}): Promise<{ bookings: BookingRequest[]; newCount: number }> {
|
}): Promise<{ bookings: BookingRequest[]; newCount: number }> {
|
||||||
try {
|
try {
|
||||||
const bookings = DATA_SOURCE === "database"
|
const bookings =
|
||||||
? await this.loadBookingsFromDB(filter)
|
DATA_SOURCE === "database"
|
||||||
: this.loadBookingsFromFile(filter);
|
? await this.loadBookingsFromDB(filter)
|
||||||
|
: this.loadBookingsFromFile(filter);
|
||||||
|
|
||||||
bookings.sort((a, b) => {
|
bookings.sort((a, b) => {
|
||||||
if (a.priority !== b.priority) return a.priority === "urgent" ? -1 : 1;
|
if (a.priority !== b.priority) return a.priority === "urgent" ? -1 : 1;
|
||||||
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
return (
|
||||||
|
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
||||||
|
|
@ -145,7 +155,10 @@ export class BookingService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rejectBooking(id: string, reason?: string): Promise<BookingRequest> {
|
public async rejectBooking(
|
||||||
|
id: string,
|
||||||
|
reason?: string
|
||||||
|
): Promise<BookingRequest> {
|
||||||
try {
|
try {
|
||||||
if (DATA_SOURCE === "database") {
|
if (DATA_SOURCE === "database") {
|
||||||
return await this.rejectBookingDB(id, reason);
|
return await this.rejectBookingDB(id, reason);
|
||||||
|
|
@ -194,9 +207,15 @@ export class BookingService {
|
||||||
scheduledTime: new Date(row.scheduledTime).toISOString(),
|
scheduledTime: new Date(row.scheduledTime).toISOString(),
|
||||||
createdAt: new Date(row.createdAt).toISOString(),
|
createdAt: new Date(row.createdAt).toISOString(),
|
||||||
updatedAt: new Date(row.updatedAt).toISOString(),
|
updatedAt: new Date(row.updatedAt).toISOString(),
|
||||||
acceptedAt: row.acceptedAt ? new Date(row.acceptedAt).toISOString() : undefined,
|
acceptedAt: row.acceptedAt
|
||||||
rejectedAt: row.rejectedAt ? new Date(row.rejectedAt).toISOString() : undefined,
|
? new Date(row.acceptedAt).toISOString()
|
||||||
completedAt: row.completedAt ? new Date(row.completedAt).toISOString() : undefined,
|
: undefined,
|
||||||
|
rejectedAt: row.rejectedAt
|
||||||
|
? new Date(row.rejectedAt).toISOString()
|
||||||
|
: undefined,
|
||||||
|
completedAt: row.completedAt
|
||||||
|
? new Date(row.completedAt).toISOString()
|
||||||
|
: undefined,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,7 +249,10 @@ export class BookingService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async rejectBookingDB(id: string, reason?: string): Promise<BookingRequest> {
|
private async rejectBookingDB(
|
||||||
|
id: string,
|
||||||
|
reason?: string
|
||||||
|
): Promise<BookingRequest> {
|
||||||
const rows = await query(
|
const rows = await query(
|
||||||
`UPDATE booking_requests
|
`UPDATE booking_requests
|
||||||
SET status = 'rejected', rejected_at = NOW(), updated_at = NOW(), rejection_reason = $2
|
SET status = 'rejected', rejected_at = NOW(), updated_at = NOW(), rejection_reason = $2
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,7 @@ class MailAccountFileService {
|
||||||
try {
|
try {
|
||||||
await fs.access(this.accountsDir);
|
await fs.access(this.accountsDir);
|
||||||
} catch {
|
} catch {
|
||||||
try {
|
await fs.mkdir(this.accountsDir, { recursive: true, mode: 0o755 });
|
||||||
await fs.mkdir(this.accountsDir, { recursive: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("메일 계정 디렉토리 생성 실패:", error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,11 +59,7 @@ export class MailReceiveBasicService {
|
||||||
try {
|
try {
|
||||||
await fs.access(this.attachmentsDir);
|
await fs.access(this.attachmentsDir);
|
||||||
} catch {
|
} catch {
|
||||||
try {
|
await fs.mkdir(this.attachmentsDir, { recursive: true, mode: 0o755 });
|
||||||
await fs.mkdir(this.attachmentsDir, { recursive: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("메일 첨부파일 디렉토리 생성 실패:", error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,13 @@ const SENT_MAIL_DIR =
|
||||||
|
|
||||||
class MailSentHistoryService {
|
class MailSentHistoryService {
|
||||||
constructor() {
|
constructor() {
|
||||||
// 디렉토리 생성 (없으면) - try-catch로 권한 에러 방지
|
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(SENT_MAIL_DIR)) {
|
if (!fs.existsSync(SENT_MAIL_DIR)) {
|
||||||
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true });
|
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true, mode: 0o755 });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("메일 발송 이력 디렉토리 생성 실패:", error);
|
console.error("메일 발송 이력 디렉토리 생성 실패:", error);
|
||||||
// 디렉토리가 이미 존재하거나 권한이 없어도 서비스는 계속 실행
|
throw error;
|
||||||
// 실제 파일 쓰기 시점에 에러 처리
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,13 +43,15 @@ class MailSentHistoryService {
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 디렉토리가 없으면 다시 시도
|
|
||||||
if (!fs.existsSync(SENT_MAIL_DIR)) {
|
if (!fs.existsSync(SENT_MAIL_DIR)) {
|
||||||
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true });
|
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true, mode: 0o755 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = path.join(SENT_MAIL_DIR, `${history.id}.json`);
|
const filePath = path.join(SENT_MAIL_DIR, `${history.id}.json`);
|
||||||
fs.writeFileSync(filePath, JSON.stringify(history, null, 2), "utf-8");
|
fs.writeFileSync(filePath, JSON.stringify(history, null, 2), {
|
||||||
|
encoding: "utf-8",
|
||||||
|
mode: 0o644,
|
||||||
|
});
|
||||||
|
|
||||||
console.log("발송 이력 저장:", history.id);
|
console.log("발송 이력 저장:", history.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -54,17 +54,13 @@ class MailTemplateFileService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 템플릿 디렉토리 생성 (없으면) - try-catch로 권한 에러 방지
|
* 템플릿 디렉토리 생성
|
||||||
*/
|
*/
|
||||||
private async ensureDirectoryExists() {
|
private async ensureDirectoryExists() {
|
||||||
try {
|
try {
|
||||||
await fs.access(this.templatesDir);
|
await fs.access(this.templatesDir);
|
||||||
} catch {
|
} catch {
|
||||||
try {
|
await fs.mkdir(this.templatesDir, { recursive: true, mode: 0o755 });
|
||||||
await fs.mkdir(this.templatesDir, { recursive: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("메일 템플릿 디렉토리 생성 실패:", error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,13 +61,20 @@ export class TodoService {
|
||||||
* 데이터 디렉토리 생성 (파일 모드)
|
* 데이터 디렉토리 생성 (파일 모드)
|
||||||
*/
|
*/
|
||||||
private ensureDataDirectory(): void {
|
private ensureDataDirectory(): void {
|
||||||
if (!fs.existsSync(TODO_DIR)) {
|
try {
|
||||||
fs.mkdirSync(TODO_DIR, { recursive: true });
|
if (!fs.existsSync(TODO_DIR)) {
|
||||||
logger.info(`📁 To-Do 데이터 디렉토리 생성: ${TODO_DIR}`);
|
fs.mkdirSync(TODO_DIR, { recursive: true, mode: 0o755 });
|
||||||
}
|
logger.info(`📁 To-Do 데이터 디렉토리 생성: ${TODO_DIR}`);
|
||||||
if (!fs.existsSync(TODO_FILE)) {
|
}
|
||||||
fs.writeFileSync(TODO_FILE, JSON.stringify([], null, 2));
|
if (!fs.existsSync(TODO_FILE)) {
|
||||||
logger.info(`📄 To-Do 파일 생성: ${TODO_FILE}`);
|
fs.writeFileSync(TODO_FILE, JSON.stringify([], null, 2), {
|
||||||
|
mode: 0o644,
|
||||||
|
});
|
||||||
|
logger.info(`📄 To-Do 파일 생성: ${TODO_FILE}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ To-Do 디렉토리 생성 실패: ${TODO_DIR}`, error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,15 +87,17 @@ export class TodoService {
|
||||||
assignedTo?: string;
|
assignedTo?: string;
|
||||||
}): Promise<TodoListResponse> {
|
}): Promise<TodoListResponse> {
|
||||||
try {
|
try {
|
||||||
const todos = DATA_SOURCE === "database"
|
const todos =
|
||||||
? await this.loadTodosFromDB(filter)
|
DATA_SOURCE === "database"
|
||||||
: this.loadTodosFromFile(filter);
|
? await this.loadTodosFromDB(filter)
|
||||||
|
: this.loadTodosFromFile(filter);
|
||||||
|
|
||||||
// 정렬: 긴급 > 우선순위 > 순서
|
// 정렬: 긴급 > 우선순위 > 순서
|
||||||
todos.sort((a, b) => {
|
todos.sort((a, b) => {
|
||||||
if (a.isUrgent !== b.isUrgent) return a.isUrgent ? -1 : 1;
|
if (a.isUrgent !== b.isUrgent) return a.isUrgent ? -1 : 1;
|
||||||
const priorityOrder = { urgent: 0, high: 1, normal: 2, low: 3 };
|
const priorityOrder = { urgent: 0, high: 1, normal: 2, low: 3 };
|
||||||
if (a.priority !== b.priority) return priorityOrder[a.priority] - priorityOrder[b.priority];
|
if (a.priority !== b.priority)
|
||||||
|
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
||||||
return a.order - b.order;
|
return a.order - b.order;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -124,7 +133,8 @@ export class TodoService {
|
||||||
await this.createTodoDB(newTodo);
|
await this.createTodoDB(newTodo);
|
||||||
} else {
|
} else {
|
||||||
const todos = this.loadTodosFromFile();
|
const todos = this.loadTodosFromFile();
|
||||||
newTodo.order = todos.length > 0 ? Math.max(...todos.map((t) => t.order)) + 1 : 0;
|
newTodo.order =
|
||||||
|
todos.length > 0 ? Math.max(...todos.map((t) => t.order)) + 1 : 0;
|
||||||
todos.push(newTodo);
|
todos.push(newTodo);
|
||||||
this.saveTodosToFile(todos);
|
this.saveTodosToFile(todos);
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +150,10 @@ export class TodoService {
|
||||||
/**
|
/**
|
||||||
* To-Do 항목 수정
|
* To-Do 항목 수정
|
||||||
*/
|
*/
|
||||||
public async updateTodo(id: string, updates: Partial<TodoItem>): Promise<TodoItem> {
|
public async updateTodo(
|
||||||
|
id: string,
|
||||||
|
updates: Partial<TodoItem>
|
||||||
|
): Promise<TodoItem> {
|
||||||
try {
|
try {
|
||||||
if (DATA_SOURCE === "database") {
|
if (DATA_SOURCE === "database") {
|
||||||
return await this.updateTodoDB(id, updates);
|
return await this.updateTodoDB(id, updates);
|
||||||
|
|
@ -231,7 +244,9 @@ export class TodoService {
|
||||||
dueDate: row.dueDate ? new Date(row.dueDate).toISOString() : undefined,
|
dueDate: row.dueDate ? new Date(row.dueDate).toISOString() : undefined,
|
||||||
createdAt: new Date(row.createdAt).toISOString(),
|
createdAt: new Date(row.createdAt).toISOString(),
|
||||||
updatedAt: new Date(row.updatedAt).toISOString(),
|
updatedAt: new Date(row.updatedAt).toISOString(),
|
||||||
completedAt: row.completedAt ? new Date(row.completedAt).toISOString() : undefined,
|
completedAt: row.completedAt
|
||||||
|
? new Date(row.completedAt).toISOString()
|
||||||
|
: undefined,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -263,7 +278,10 @@ export class TodoService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateTodoDB(id: string, updates: Partial<TodoItem>): Promise<TodoItem> {
|
private async updateTodoDB(
|
||||||
|
id: string,
|
||||||
|
updates: Partial<TodoItem>
|
||||||
|
): Promise<TodoItem> {
|
||||||
const setClauses: string[] = ["updated_at = NOW()"];
|
const setClauses: string[] = ["updated_at = NOW()"];
|
||||||
const params: any[] = [];
|
const params: any[] = [];
|
||||||
let paramIndex = 1;
|
let paramIndex = 1;
|
||||||
|
|
@ -327,12 +345,17 @@ export class TodoService {
|
||||||
dueDate: row.dueDate ? new Date(row.dueDate).toISOString() : undefined,
|
dueDate: row.dueDate ? new Date(row.dueDate).toISOString() : undefined,
|
||||||
createdAt: new Date(row.createdAt).toISOString(),
|
createdAt: new Date(row.createdAt).toISOString(),
|
||||||
updatedAt: new Date(row.updatedAt).toISOString(),
|
updatedAt: new Date(row.updatedAt).toISOString(),
|
||||||
completedAt: row.completedAt ? new Date(row.completedAt).toISOString() : undefined,
|
completedAt: row.completedAt
|
||||||
|
? new Date(row.completedAt).toISOString()
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deleteTodoDB(id: string): Promise<void> {
|
private async deleteTodoDB(id: string): Promise<void> {
|
||||||
const rows = await query("DELETE FROM todo_items WHERE id = $1 RETURNING id", [id]);
|
const rows = await query(
|
||||||
|
"DELETE FROM todo_items WHERE id = $1 RETURNING id",
|
||||||
|
[id]
|
||||||
|
);
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
throw new Error(`To-Do 항목을 찾을 수 없습니다: ${id}`);
|
throw new Error(`To-Do 항목을 찾을 수 없습니다: ${id}`);
|
||||||
}
|
}
|
||||||
|
|
@ -443,7 +466,10 @@ export class TodoService {
|
||||||
inProgress: todos.filter((t) => t.status === "in_progress").length,
|
inProgress: todos.filter((t) => t.status === "in_progress").length,
|
||||||
completed: todos.filter((t) => t.status === "completed").length,
|
completed: todos.filter((t) => t.status === "completed").length,
|
||||||
urgent: todos.filter((t) => t.isUrgent).length,
|
urgent: todos.filter((t) => t.isUrgent).length,
|
||||||
overdue: todos.filter((t) => t.dueDate && new Date(t.dueDate) < now && t.status !== "completed").length,
|
overdue: todos.filter(
|
||||||
|
(t) =>
|
||||||
|
t.dueDate && new Date(t.dueDate) < now && t.status !== "completed"
|
||||||
|
).length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,11 @@ COPY --from=build /app/dist ./dist
|
||||||
# Copy package files
|
# Copy package files
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Create logs, uploads, and data directories and set permissions (use existing node user with UID 1000)
|
# 루트 디렉토리만 생성하고 node 유저에게 쓰기 권한 부여
|
||||||
RUN mkdir -p logs \
|
# 하위 디렉토리는 애플리케이션이 런타임에 자동 생성
|
||||||
uploads/mail-attachments \
|
RUN mkdir -p logs uploads data && \
|
||||||
uploads/mail-templates \
|
chown -R node:node /app && \
|
||||||
uploads/mail-accounts \
|
chmod -R 755 /app
|
||||||
data/mail-sent && \
|
|
||||||
chown -R node:node logs uploads data && \
|
|
||||||
chmod -R 755 logs uploads data
|
|
||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
USER node
|
USER node
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,11 @@ COPY --from=build /app/dist ./dist
|
||||||
# Copy package files
|
# Copy package files
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Create logs directory and set permissions
|
# 루트 디렉토리만 생성하고 appuser에게 쓰기 권한 부여
|
||||||
RUN mkdir -p logs && chown -R appuser:appgroup logs && chmod -R 755 logs
|
# 하위 디렉토리는 애플리케이션이 런타임에 자동 생성
|
||||||
|
RUN mkdir -p logs uploads data && \
|
||||||
# uploads 디렉토리는 볼륨으로 마운트되므로 생성하지 않음
|
chown -R appuser:appgroup /app && \
|
||||||
|
chmod -R 755 /app
|
||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
USER appuser
|
USER appuser
|
||||||
|
|
|
||||||
|
|
@ -20,17 +20,15 @@ echo ""
|
||||||
echo "[1/6] Git 최신 코드 가져오기..."
|
echo "[1/6] Git 최신 코드 가져오기..."
|
||||||
git pull origin main
|
git pull origin main
|
||||||
|
|
||||||
# 호스트 디렉토리 준비
|
# 호스트 디렉토리 준비 (볼륨 마운트용 루트 디렉토리만 생성)
|
||||||
echo ""
|
echo ""
|
||||||
echo "[2/6] 호스트 디렉토리 준비..."
|
echo "[2/6] 호스트 디렉토리 준비..."
|
||||||
mkdir -p /home/vexplor/backend_data/data/mail-sent
|
mkdir -p /home/vexplor/backend_data/uploads
|
||||||
mkdir -p /home/vexplor/backend_data/uploads/mail-attachments
|
mkdir -p /home/vexplor/backend_data/data
|
||||||
mkdir -p /home/vexplor/backend_data/uploads/mail-templates
|
|
||||||
mkdir -p /home/vexplor/backend_data/uploads/mail-accounts
|
|
||||||
mkdir -p /home/vexplor/frontend_data
|
mkdir -p /home/vexplor/frontend_data
|
||||||
chmod -R 755 /home/vexplor/backend_data
|
chmod -R 755 /home/vexplor/backend_data
|
||||||
chmod -R 755 /home/vexplor/frontend_data
|
chmod -R 755 /home/vexplor/frontend_data
|
||||||
echo "디렉토리 생성 완료 (mail-sent, mail-attachments, mail-templates, mail-accounts, frontend)"
|
echo "볼륨 마운트 디렉토리 생성 완료 (하위 디렉토리는 컨테이너가 자동 생성)"
|
||||||
|
|
||||||
# 기존 컨테이너 중지 및 제거
|
# 기존 컨테이너 중지 및 제거
|
||||||
echo ""
|
echo ""
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue