Initial commit: AI platform app (server, views, lib, data, deploy docs)
Made-with: Cursor
This commit is contained in:
62
scripts/apply-schema.js
Normal file
62
scripts/apply-schema.js
Normal file
@@ -0,0 +1,62 @@
|
||||
require("dotenv").config({ quiet: true });
|
||||
|
||||
const fs = require("fs").promises;
|
||||
const path = require("path");
|
||||
const { Pool } = require("pg");
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, "..");
|
||||
const SCHEMA_PATH = path.join(ROOT_DIR, "db", "schema.sql");
|
||||
|
||||
const getConfig = () => ({
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT || 5432),
|
||||
database: process.env.DB_DATABASE,
|
||||
user: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
ssl: process.env.DB_SSL === "1" ? { rejectUnauthorized: false } : false,
|
||||
});
|
||||
|
||||
const validateConfig = (config) => {
|
||||
const missing = ["host", "database", "user", "password"].filter((key) => !config[key]);
|
||||
if (missing.length) {
|
||||
throw new Error(`Missing PostgreSQL envs: ${missing.join(", ")}`);
|
||||
}
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
const config = getConfig();
|
||||
validateConfig(config);
|
||||
|
||||
const sql = await fs.readFile(SCHEMA_PATH, "utf-8");
|
||||
const pool = new Pool(config);
|
||||
try {
|
||||
await pool.query("SELECT 1");
|
||||
await pool.query(sql);
|
||||
console.log("Schema applied successfully.");
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
};
|
||||
|
||||
run().catch((error) => {
|
||||
console.error("Failed to apply schema:", error.message);
|
||||
const msg = String(error.message || "");
|
||||
if (error.code === "28P01" || /password authentication failed/i.test(msg)) {
|
||||
console.error(
|
||||
"\n[DB 인증 실패] .env의 DB_USERNAME·DB_PASSWORD가 PostgreSQL에 등록된 사용자·비밀번호와 일치하는지 확인하세요.",
|
||||
);
|
||||
console.error(
|
||||
" (원격 DB면 관리자에게 계정을 확인하고, 로컬이면 `sudo -u postgres psql`에서 \\password 사용자명 으로 비밀번호를 맞추세요.)",
|
||||
);
|
||||
} else if (/ECONNREFUSED|ENOTFOUND/i.test(msg) || error.code === "ECONNREFUSED") {
|
||||
console.error("\n[DB 연결 불가] DB_HOST·DB_PORT가 맞는지, 방화벽·PostgreSQL listen_addresses를 확인하세요.");
|
||||
} else if (error.code === "28000" || /role .* does not exist/i.test(msg)) {
|
||||
console.error(
|
||||
"\n[DB 사용자 없음] .env의 DB_USERNAME에 해당하는 PostgreSQL 역할(role)이 서버에 없습니다.",
|
||||
);
|
||||
console.error(
|
||||
" 슈퍼유저로 접속해 CREATE ROLE ... LOGIN PASSWORD '...'; 및 GRANT를 실행하거나, db/bootstrap-role.sql.example을 참고하세요.",
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
32
scripts/check-ax-schema.js
Normal file
32
scripts/check-ax-schema.js
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* ax_assignments 테이블 컬럼 타입 확인 (PostgreSQL)
|
||||
* 사용: node scripts/check-ax-schema.js
|
||||
*/
|
||||
require("dotenv").config({ path: require("path").join(__dirname, "..", ".env") });
|
||||
const { Pool } = require("pg");
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST || "localhost",
|
||||
port: Number(process.env.DB_PORT || 5432),
|
||||
database: process.env.DB_DATABASE || "ai_web_platform",
|
||||
user: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
});
|
||||
|
||||
async function main() {
|
||||
const res = await pool.query(`
|
||||
SELECT column_name, data_type, udt_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'ax_assignments'
|
||||
ORDER BY ordinal_position
|
||||
`);
|
||||
console.log("ax_assignments columns:");
|
||||
res.rows.forEach((r) => console.log(` ${r.column_name}: ${r.data_type} (${r.udt_name})`));
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
269
scripts/migrate-db-ncue-to-env.js
Normal file
269
scripts/migrate-db-ncue-to-env.js
Normal file
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* .env.ncue(원본 DB) → .env(대상 DB) 데이터 복사
|
||||
*
|
||||
* 학습센터(강의 목록): lectures 테이블
|
||||
* 선택: lecture_thumbnail_events (썸네일 작업 로그)
|
||||
*
|
||||
* 사용 전:
|
||||
* 1) 대상 DB에 스키마 적용: node scripts/apply-schema.js (.env 기준)
|
||||
* 2) 대상 서버에 강의 파일 동기화(별도): DB만으로는 PPT/슬라이드/썸네일 바이너리가 없음
|
||||
* - resources/lecture/ (업로드 원본 PPT 등)
|
||||
* - uploads/thumbnails/ (썸네일)
|
||||
* - uploads/slides/<강의UUID>/ (슬라이드 이미지)
|
||||
*
|
||||
* 실행 (저장소 루트에서):
|
||||
* node scripts/migrate-db-ncue-to-env.js
|
||||
* node scripts/migrate-db-ncue-to-env.js --dry-run
|
||||
* node scripts/migrate-db-ncue-to-env.js --with-thumbnail-events
|
||||
*
|
||||
* 대상에 강의만 원본과 동일하게 맞추고(기존 행 삭제 후 복사):
|
||||
* node scripts/migrate-db-ncue-to-env.js --truncate-target
|
||||
*
|
||||
* 경로 변경:
|
||||
* SOURCE_ENV=.env.ncue TARGET_ENV=.env node scripts/migrate-db-ncue-to-env.js
|
||||
*/
|
||||
require("dotenv").config({ quiet: true });
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const dotenv = require("dotenv");
|
||||
const { Pool } = require("pg");
|
||||
|
||||
const ROOT = path.join(__dirname, "..");
|
||||
|
||||
const LECTURE_COLUMNS = [
|
||||
"id",
|
||||
"type",
|
||||
"title",
|
||||
"description",
|
||||
"tags",
|
||||
"youtube_url",
|
||||
"file_name",
|
||||
"original_name",
|
||||
"preview_title",
|
||||
"slide_count",
|
||||
"thumbnail_url",
|
||||
"thumbnail_status",
|
||||
"thumbnail_retry_count",
|
||||
"thumbnail_error",
|
||||
"thumbnail_updated_at",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"list_section",
|
||||
"news_url",
|
||||
];
|
||||
|
||||
const THUMB_EVENT_COLUMNS = [
|
||||
"id",
|
||||
"occurred_at",
|
||||
"event_type",
|
||||
"lecture_id",
|
||||
"lecture_title",
|
||||
"reason",
|
||||
"force_flag",
|
||||
"queue_size_after",
|
||||
"retry_count",
|
||||
"duration_ms",
|
||||
"error_text",
|
||||
];
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = { dryRun: false, withThumbnailEvents: false, truncateTarget: false };
|
||||
for (const a of argv) {
|
||||
if (a === "--dry-run") out.dryRun = true;
|
||||
if (a === "--with-thumbnail-events") out.withThumbnailEvents = true;
|
||||
if (a === "--truncate-target") out.truncateTarget = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function loadEnvFile(filePath) {
|
||||
const abs = path.isAbsolute(filePath) ? filePath : path.join(ROOT, filePath);
|
||||
if (!fs.existsSync(abs)) {
|
||||
throw new Error(`환경 파일이 없습니다: ${abs}`);
|
||||
}
|
||||
return dotenv.parse(fs.readFileSync(abs, "utf8"));
|
||||
}
|
||||
|
||||
function poolConfigFromEnv(e) {
|
||||
const missing = ["DB_HOST", "DB_DATABASE", "DB_USERNAME", "DB_PASSWORD"].filter((k) => !e[k]);
|
||||
if (missing.length) {
|
||||
throw new Error(`DB 설정 누락: ${missing.join(", ")}`);
|
||||
}
|
||||
return {
|
||||
host: e.DB_HOST,
|
||||
port: Number(e.DB_PORT || 5432),
|
||||
database: e.DB_DATABASE,
|
||||
user: e.DB_USERNAME,
|
||||
password: e.DB_PASSWORD,
|
||||
ssl: e.DB_SSL === "1" ? { rejectUnauthorized: false } : false,
|
||||
};
|
||||
}
|
||||
|
||||
function placeholders(n) {
|
||||
return Array.from({ length: n }, (_, i) => `$${i + 1}`).join(", ");
|
||||
}
|
||||
|
||||
function buildLectureUpsert() {
|
||||
const cols = LECTURE_COLUMNS;
|
||||
const ph = placeholders(cols.length);
|
||||
const updates = cols
|
||||
.filter((c) => c !== "id")
|
||||
.map((c) => `${c} = EXCLUDED.${c}`)
|
||||
.join(", ");
|
||||
return `
|
||||
INSERT INTO lectures (${cols.join(", ")})
|
||||
VALUES (${ph})
|
||||
ON CONFLICT (id) DO UPDATE SET ${updates}
|
||||
`;
|
||||
}
|
||||
|
||||
function buildThumbEventUpsert() {
|
||||
const cols = THUMB_EVENT_COLUMNS;
|
||||
const ph = placeholders(cols.length);
|
||||
const updates = cols
|
||||
.filter((c) => c !== "id")
|
||||
.map((c) => `${c} = EXCLUDED.${c}`)
|
||||
.join(", ");
|
||||
return `
|
||||
INSERT INTO lecture_thumbnail_events (${cols.join(", ")})
|
||||
VALUES (${ph})
|
||||
ON CONFLICT (id) DO UPDATE SET ${updates}
|
||||
`;
|
||||
}
|
||||
|
||||
function rowValues(row, columns) {
|
||||
return columns.map((c) => row[c]);
|
||||
}
|
||||
|
||||
async function truncateTargetTables(targetPool, dryRun) {
|
||||
if (dryRun) {
|
||||
console.log(
|
||||
"[truncate] dry-run: lecture_thumbnail_events, lectures TRUNCATE 생략"
|
||||
);
|
||||
return;
|
||||
}
|
||||
await targetPool.query("TRUNCATE lecture_thumbnail_events");
|
||||
await targetPool.query("TRUNCATE lectures");
|
||||
console.log("[truncate] 대상 lecture_thumbnail_events, lectures 비움");
|
||||
}
|
||||
|
||||
async function migrateLectures(sourcePool, targetPool, dryRun) {
|
||||
const { rows } = await sourcePool.query(
|
||||
`SELECT ${LECTURE_COLUMNS.join(", ")} FROM lectures ORDER BY created_at ASC`
|
||||
);
|
||||
console.log(`[lectures] 원본 행 수: ${rows.length}`);
|
||||
if (dryRun) {
|
||||
console.log("[lectures] dry-run: INSERT 생략");
|
||||
return rows.length;
|
||||
}
|
||||
const sql = buildLectureUpsert();
|
||||
const client = await targetPool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
let n = 0;
|
||||
for (const row of rows) {
|
||||
await client.query(sql, rowValues(row, LECTURE_COLUMNS));
|
||||
n++;
|
||||
}
|
||||
await client.query("COMMIT");
|
||||
console.log(`[lectures] 대상 DB 반영 완료: ${n}건`);
|
||||
return n;
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
throw e;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateThumbnailEvents(sourcePool, targetPool, dryRun) {
|
||||
const { rows } = await sourcePool.query(
|
||||
`SELECT ${THUMB_EVENT_COLUMNS.join(", ")} FROM lecture_thumbnail_events ORDER BY occurred_at ASC`
|
||||
);
|
||||
console.log(`[lecture_thumbnail_events] 원본 행 수: ${rows.length}`);
|
||||
if (dryRun) {
|
||||
console.log("[lecture_thumbnail_events] dry-run: INSERT 생략");
|
||||
return rows.length;
|
||||
}
|
||||
const sql = buildThumbEventUpsert();
|
||||
const client = await targetPool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
let n = 0;
|
||||
for (const row of rows) {
|
||||
await client.query(sql, rowValues(row, THUMB_EVENT_COLUMNS));
|
||||
n++;
|
||||
}
|
||||
await client.query("COMMIT");
|
||||
console.log(`[lecture_thumbnail_events] 대상 DB 반영 완료: ${n}건`);
|
||||
return n;
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
throw e;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
function printFileHint() {
|
||||
console.log(`
|
||||
[파일 동기화 안내] DB만 복사하면 메타데이터만 옮겨집니다. 강의 PPT/슬라이드/썸네일이 비어 있으면
|
||||
원본 서버(또는 .env.ncue가 가리키는 환경의 디스크)에서 대상(ai.xavis.co.kr) 배포 디렉터리로 복사하세요.
|
||||
|
||||
예시 (원본 호스트에서 대상으로 rsync — 경로는 실제 배포에 맞게 조정):
|
||||
rsync -avz ./resources/lecture/ user@ai-host:/path/to/webplatform/resources/lecture/
|
||||
rsync -avz ./uploads/thumbnails/ user@ai-host:/path/to/webplatform/uploads/thumbnails/
|
||||
rsync -avz ./uploads/slides/ user@ai-host:/path/to/webplatform/uploads/slides/
|
||||
`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const sourcePath = process.env.SOURCE_ENV || ".env.ncue";
|
||||
const targetPath = process.env.TARGET_ENV || ".env";
|
||||
|
||||
const sourceEnv = loadEnvFile(sourcePath);
|
||||
const targetEnv = loadEnvFile(targetPath);
|
||||
|
||||
const srcCfg = poolConfigFromEnv(sourceEnv);
|
||||
const tgtCfg = poolConfigFromEnv(targetEnv);
|
||||
|
||||
console.log(`원본: ${sourcePath} → ${srcCfg.host}/${srcCfg.database}`);
|
||||
console.log(`대상: ${targetPath} → ${tgtCfg.host}/${tgtCfg.database}`);
|
||||
if (args.dryRun) {
|
||||
console.log("모드: DRY-RUN (쓰기 없음)\n");
|
||||
}
|
||||
|
||||
const sourcePool = new Pool(srcCfg);
|
||||
const targetPool = new Pool(tgtCfg);
|
||||
|
||||
try {
|
||||
await sourcePool.query("SELECT 1");
|
||||
await targetPool.query("SELECT 1");
|
||||
} catch (e) {
|
||||
console.error("DB 연결 실패:", e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
if (args.truncateTarget) {
|
||||
await truncateTargetTables(targetPool, args.dryRun);
|
||||
}
|
||||
await migrateLectures(sourcePool, targetPool, args.dryRun);
|
||||
if (args.withThumbnailEvents) {
|
||||
await migrateThumbnailEvents(sourcePool, targetPool, args.dryRun);
|
||||
}
|
||||
printFileHint();
|
||||
console.log("완료.");
|
||||
} catch (e) {
|
||||
console.error("마이그레이션 실패:", e.message || e);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await sourcePool.end();
|
||||
await targetPool.end();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
69
scripts/normalize-stored-meeting-minutes.js
Normal file
69
scripts/normalize-stored-meeting-minutes.js
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* DB에 저장된 회의록 generated_minutes에 prepareMeetingMinutesForApi(말미 제거·제목 승격)를
|
||||
* 일괄 적용합니다. 서버 재시작 없이 기존 데이터를 정리할 때 사용합니다.
|
||||
*
|
||||
* 사용: node scripts/normalize-stored-meeting-minutes.js
|
||||
* 요구: .env에 DB_HOST, DB_DATABASE, DB_USERNAME, DB_PASSWORD 등 (apply-schema와 동일)
|
||||
*/
|
||||
require("dotenv").config({ quiet: true });
|
||||
|
||||
const { Pool } = require("pg");
|
||||
const path = require("path");
|
||||
|
||||
const { prepareMeetingMinutesForApi } = require(path.join(__dirname, "..", "lib", "meeting-minutes"));
|
||||
const { extractMeetingSummary } = require(path.join(__dirname, "..", "lib", "meeting-minutes-summary"));
|
||||
|
||||
const getConfig = () => ({
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT || 5432),
|
||||
database: process.env.DB_DATABASE,
|
||||
user: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
ssl: process.env.DB_SSL === "1" ? { rejectUnauthorized: false } : false,
|
||||
});
|
||||
|
||||
const validateConfig = (config) => {
|
||||
const missing = ["host", "database", "user", "password"].filter((key) => !config[key]);
|
||||
if (missing.length) {
|
||||
throw new Error(`Missing PostgreSQL envs: ${missing.join(", ")}`);
|
||||
}
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const config = getConfig();
|
||||
validateConfig(config);
|
||||
const pool = new Pool(config);
|
||||
let updated = 0;
|
||||
let unchanged = 0;
|
||||
try {
|
||||
const r = await pool.query(
|
||||
`SELECT id, user_email, generated_minutes FROM meeting_ai_meetings ORDER BY created_at ASC`
|
||||
);
|
||||
for (const row of r.rows) {
|
||||
const raw = row.generated_minutes != null ? String(row.generated_minutes) : "";
|
||||
const next = prepareMeetingMinutesForApi(raw);
|
||||
if (next === raw) {
|
||||
unchanged++;
|
||||
continue;
|
||||
}
|
||||
const summaryText = extractMeetingSummary(next, 1200);
|
||||
await pool.query(
|
||||
`UPDATE meeting_ai_meetings
|
||||
SET generated_minutes = $1, summary_text = $2, updated_at = NOW()
|
||||
WHERE id = $3::uuid AND user_email = $4`,
|
||||
[next, summaryText || null, row.id, row.user_email]
|
||||
);
|
||||
updated++;
|
||||
console.log("updated:", row.id, row.user_email);
|
||||
}
|
||||
console.log(`Done. updated=${updated}, unchanged=${unchanged}, total=${r.rows.length}`);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
42
scripts/test-ax-apply.js
Normal file
42
scripts/test-ax-apply.js
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* AX 과제 신청 API 테스트 (필수값만)
|
||||
* 사용: node scripts/test-ax-apply.js
|
||||
* ENABLE_POSTGRES=0: JSON 폴백 사용 (DB 불필요)
|
||||
* ENABLE_POSTGRES=1: PostgreSQL 사용 (기본값)
|
||||
*/
|
||||
require("dotenv").config({ path: require("path").join(__dirname, "..", ".env") });
|
||||
|
||||
const BASE = process.env.TEST_BASE_URL || "http://localhost:8030";
|
||||
|
||||
const minimalPayload = {
|
||||
department: "테스트부서",
|
||||
name: "홍길동",
|
||||
workProcessDescription: "테스트",
|
||||
painPoint: "테스트",
|
||||
currentTimeSpent: "30분",
|
||||
errorRateBefore: "5%",
|
||||
reasonToSolve: "테스트",
|
||||
aiExpectation: "테스트",
|
||||
outputType: "테스트",
|
||||
participationPledge: true,
|
||||
};
|
||||
|
||||
async function main() {
|
||||
console.log("POST", BASE + "/api/ax-apply");
|
||||
console.log("Payload (필수값만):", JSON.stringify(minimalPayload, null, 2));
|
||||
const res = await fetch(BASE + "/api/ax-apply", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(minimalPayload),
|
||||
});
|
||||
const body = await res.json().catch(() => ({}));
|
||||
console.log("Status:", res.status);
|
||||
console.log("Response:", JSON.stringify(body, null, 2));
|
||||
process.exit(res.ok && body.ok ? 0 : 1);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
64
scripts/test-ax-insert.js
Normal file
64
scripts/test-ax-insert.js
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* ax_assignments INSERT 직접 테스트 (배열 SQL 리터럴 방식)
|
||||
*/
|
||||
require("dotenv").config({ path: require("path").join(__dirname, "..", ".env") });
|
||||
const { Pool } = require("pg");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT || 5432),
|
||||
database: process.env.DB_DATABASE,
|
||||
user: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
});
|
||||
|
||||
async function main() {
|
||||
const id = uuidv4();
|
||||
const now = new Date();
|
||||
|
||||
const toArrayLiteralSafe = (v) => {
|
||||
if (v === "" || typeof v === "string" || v == null) return "'{}'";
|
||||
const arr = Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
||||
if (arr.length === 0) return "'{}'";
|
||||
const escaped = arr.map((s) => `"${String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`);
|
||||
return `'{${escaped.join(",")}}'`;
|
||||
};
|
||||
|
||||
const dataTypesLit = "'{}'";
|
||||
const qualitativeLit = "'{}'";
|
||||
const techStackLit = "'{}'";
|
||||
const risksLit = "'{}'";
|
||||
|
||||
const sql = `INSERT INTO ax_assignments (
|
||||
id, department, name, employee_id, position, phone, email,
|
||||
work_process_description, pain_point, current_time_spent, error_rate_before,
|
||||
collaboration_depts, reason_to_solve, ai_expectation, output_type, automation_level,
|
||||
data_readiness, data_location, personal_info, data_quality, data_count, data_types,
|
||||
time_reduction, error_reduction, volume_increase, cost_reduction, response_time,
|
||||
other_metrics, annual_savings, labor_replacement, revenue_increase, other_effects,
|
||||
qualitative_effects, tech_stack, risks, risk_detail, participation_pledge, status,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
$1::uuid, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16,
|
||||
$17, $18, $19, $20, $21, ${dataTypesLit}::text[], $22, $23, $24, $25, $26, $27, $28, $29, $30,
|
||||
$31, ${qualitativeLit}::text[], ${techStackLit}::text[], ${risksLit}::text[], $32, $33, $34, $35, $36
|
||||
)`;
|
||||
|
||||
const params = [
|
||||
... [id, "테스트부서", "홍길동", "", "", "", "", "테스트", "테스트", "30분", "5%", "", "테스트", "테스트", "테스트", "", "", "", "", "", "" ],
|
||||
... [ "", "", "", "", "", "", "", "", "", "" ],
|
||||
... [ "", false, "신청", now, now ],
|
||||
];
|
||||
|
||||
console.log("SQL preview:", sql.substring(200, 400));
|
||||
const res = await pool.query(sql, params);
|
||||
console.log("OK, inserted:", id);
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("Error:", e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user