SOY NV AI commited on
Commit ยท
eb0b3d8
1
Parent(s): c1b3f58
Update: UI adjustments in personal management and various improvements
Browse files- agent_skills/notion_db_skill.ts +221 -0
- app/core/logger.py +12 -1
- app/database.py +17 -0
- app/migrations.py +10 -0
- app/routes.py +180 -114
- check_has_more.py +35 -0
- check_notion_token.py +33 -0
- debug_dbs.py +38 -0
- force_update_menu.py +41 -0
- restart_server.ps1 +2 -2
- start_server_background.ps1 +8 -61
- templates/admin_settings.html +4 -2
- templates/agent_notion_db.html +7 -6
- templates/webnovels.html +667 -1
- templates/webtoon_personal_management.html +406 -92
agent_skills/notion_db_skill.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Notion DB ์ฝ๊ธฐ/์ฐ๊ธฐ์ฉ Agent Skill (Tool) ์ค์ผ๋ ํค
|
| 3 |
+
*
|
| 4 |
+
* - ์ด ํ๋ก์ ํธ๋ Python(Flask) ๊ธฐ๋ฐ์ด์ง๋ง, Gemini Tool/Skill ์ ์๋ TS๋ก๋ ์์ฑํด ๋ ์ ์์ด
|
| 5 |
+
* ์ถํ Node ๋ฐํ์/ํด์ฒด์ธ์ผ๋ก ์ด์ํ๊ฑฐ๋ ๋ณ๋ ์์ปค์์ ์คํํ๊ธฐ ์ข์ต๋๋ค.
|
| 6 |
+
*
|
| 7 |
+
* ํ์ ํจํค์ง(์์):
|
| 8 |
+
* - @notionhq/client
|
| 9 |
+
*/
|
| 10 |
+
|
| 11 |
+
// ----------------------------
|
| 12 |
+
// 1) Gemini๊ฐ ์ดํดํ "์ญํ " ํ ๋ฌธ์ฅ
|
| 13 |
+
// ----------------------------
|
| 14 |
+
export const NOTION_DB_SKILL_ROLE_SENTENCE =
|
| 15 |
+
"Notion์ ํน์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ํ์ด์ง๋ฅผ ์กฐ๊ฑด์ผ๋ก ์กฐํํ๊ณ , ์ ํ์ด์ง ์์ฑ/๊ธฐ์กด ํ์ด์ง ์
๋ฐ์ดํธ/์์นด์ด๋ธ๋ฅผ ์ํํด ๋ฐ์ดํฐ๋ฅผ ๋๊ธฐํํ๋ ์คํฌ์ด๋ค.";
|
| 16 |
+
|
| 17 |
+
// ----------------------------
|
| 18 |
+
// 2) Agent Tool input_schema (JSON Schema)
|
| 19 |
+
// ----------------------------
|
| 20 |
+
export const NOTION_DB_SKILL_INPUT_SCHEMA = {
|
| 21 |
+
type: "object",
|
| 22 |
+
additionalProperties: false,
|
| 23 |
+
properties: {
|
| 24 |
+
operation: {
|
| 25 |
+
type: "string",
|
| 26 |
+
description: "์ํํ ์์
",
|
| 27 |
+
enum: ["query", "create", "update", "upsert", "archive"],
|
| 28 |
+
},
|
| 29 |
+
// ํน์ DB ์ด๋ฆ(ํ์/๋ก๊ทธ์ฉ). ์ค์ ์คํ์ database_id๊ฐ ์ฐ์ ์
๋๋ค.
|
| 30 |
+
database_name: {
|
| 31 |
+
type: "string",
|
| 32 |
+
description: "๋์ Notion DB ์ด๋ฆ(ํ์์ฉ)",
|
| 33 |
+
},
|
| 34 |
+
// ๊ณ ์ DB ์ต์ ํ๋ฅผ ์ํด ๋ณดํต์ ์๋ฒ/์ปจํ
์คํธ์ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ๋๊ณ , ํ์ ์ override ํฉ๋๋ค.
|
| 35 |
+
database_id: {
|
| 36 |
+
type: "string",
|
| 37 |
+
description: "๋์ Notion Database ID",
|
| 38 |
+
},
|
| 39 |
+
// query ์ ์ฉ
|
| 40 |
+
query: {
|
| 41 |
+
type: "object",
|
| 42 |
+
additionalProperties: false,
|
| 43 |
+
description: "Notion DB query ์ต์
",
|
| 44 |
+
properties: {
|
| 45 |
+
filter: {
|
| 46 |
+
type: "object",
|
| 47 |
+
description: "Notion API filter ๊ฐ์ฒด(๊ทธ๋๋ก ์ ๋ฌ)",
|
| 48 |
+
},
|
| 49 |
+
sorts: {
|
| 50 |
+
type: "array",
|
| 51 |
+
description: "Notion API sorts ๋ฐฐ์ด(๊ทธ๋๋ก ์ ๋ฌ)",
|
| 52 |
+
items: { type: "object" },
|
| 53 |
+
},
|
| 54 |
+
page_size: {
|
| 55 |
+
type: "integer",
|
| 56 |
+
minimum: 1,
|
| 57 |
+
maximum: 100,
|
| 58 |
+
description: "ํ ๋ฒ์ ๊ฐ์ ธ์ฌ ๊ฒฐ๊ณผ ์(1~100)",
|
| 59 |
+
},
|
| 60 |
+
},
|
| 61 |
+
},
|
| 62 |
+
// create/update/upsert ์ ์ฉ
|
| 63 |
+
properties: {
|
| 64 |
+
type: "object",
|
| 65 |
+
description:
|
| 66 |
+
"Notion Page properties ๊ฐ์ฒด(๊ทธ๋๋ก ์ ๋ฌ). ์: { Name: { title: [{ text: { content: '...' } }] } }",
|
| 67 |
+
},
|
| 68 |
+
// update/archive ์ ์ฉ
|
| 69 |
+
page_id: {
|
| 70 |
+
type: "string",
|
| 71 |
+
description: "๋์ Notion Page ID (update/archive)",
|
| 72 |
+
},
|
| 73 |
+
// upsert ์ ์ฉ(์ค์ผ๋ ํค์์๋ ๊ตฌ์กฐ๋ง ์ ์)
|
| 74 |
+
upsert: {
|
| 75 |
+
type: "object",
|
| 76 |
+
additionalProperties: false,
|
| 77 |
+
description:
|
| 78 |
+
"์
์ํธ ํค(์ ๋ํฌ ์์ฑ) ๊ธฐ๋ฐ์ผ๋ก ์์ผ๋ฉด update, ์์ผ๋ฉด create",
|
| 79 |
+
properties: {
|
| 80 |
+
unique_property_name: {
|
| 81 |
+
type: "string",
|
| 82 |
+
description: "์ ๋ํฌ ํค๋ก ์ฌ์ฉํ Notion ์์ฑ ์ด๋ฆ(์: ExternalId)",
|
| 83 |
+
},
|
| 84 |
+
unique_property_value: {
|
| 85 |
+
type: "string",
|
| 86 |
+
description: "์ ๋ํฌ ํค ๊ฐ",
|
| 87 |
+
},
|
| 88 |
+
},
|
| 89 |
+
required: ["unique_property_name", "unique_property_value"],
|
| 90 |
+
},
|
| 91 |
+
},
|
| 92 |
+
required: ["operation"],
|
| 93 |
+
} as const;
|
| 94 |
+
|
| 95 |
+
// ----------------------------
|
| 96 |
+
// 3) TypeScript ์ธํฐํ์ด์ค + execute ์ค์ผ๋ ํค
|
| 97 |
+
// ----------------------------
|
| 98 |
+
|
| 99 |
+
export type NotionDbSkillOperation =
|
| 100 |
+
| "query"
|
| 101 |
+
| "create"
|
| 102 |
+
| "update"
|
| 103 |
+
| "upsert"
|
| 104 |
+
| "archive";
|
| 105 |
+
|
| 106 |
+
export type NotionDbSkillInput = {
|
| 107 |
+
operation: NotionDbSkillOperation;
|
| 108 |
+
database_name?: string;
|
| 109 |
+
database_id?: string;
|
| 110 |
+
query?: {
|
| 111 |
+
filter?: Record<string, unknown>;
|
| 112 |
+
sorts?: Array<Record<string, unknown>>;
|
| 113 |
+
page_size?: number;
|
| 114 |
+
};
|
| 115 |
+
properties?: Record<string, unknown>;
|
| 116 |
+
page_id?: string;
|
| 117 |
+
upsert?: {
|
| 118 |
+
unique_property_name: string;
|
| 119 |
+
unique_property_value: string;
|
| 120 |
+
};
|
| 121 |
+
};
|
| 122 |
+
|
| 123 |
+
export type NotionDbSkillOutput = {
|
| 124 |
+
operation: NotionDbSkillOperation;
|
| 125 |
+
database_id?: string;
|
| 126 |
+
database_name?: string;
|
| 127 |
+
result: unknown;
|
| 128 |
+
};
|
| 129 |
+
|
| 130 |
+
export interface AgentSkill<TInput, TOutput> {
|
| 131 |
+
name: string;
|
| 132 |
+
description: string;
|
| 133 |
+
input_schema: unknown;
|
| 134 |
+
execute(input: TInput, ctx: unknown): Promise<TOutput>;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
export type NotionSkillContext = {
|
| 138 |
+
notionToken: string;
|
| 139 |
+
defaultDatabaseId?: string;
|
| 140 |
+
defaultDatabaseName?: string;
|
| 141 |
+
};
|
| 142 |
+
|
| 143 |
+
export const notionDbReadWriteSkill: AgentSkill<
|
| 144 |
+
NotionDbSkillInput,
|
| 145 |
+
NotionDbSkillOutput
|
| 146 |
+
> = {
|
| 147 |
+
name: "notion.db.read_write",
|
| 148 |
+
description: NOTION_DB_SKILL_ROLE_SENTENCE,
|
| 149 |
+
input_schema: NOTION_DB_SKILL_INPUT_SCHEMA,
|
| 150 |
+
|
| 151 |
+
async execute(input, ctx) {
|
| 152 |
+
// lazy import: ์ค์ ๋ฐํ์์์๋ง @notionhq/client ํ์
|
| 153 |
+
const { Client } = await import("@notionhq/client");
|
| 154 |
+
|
| 155 |
+
const context = ctx as NotionSkillContext;
|
| 156 |
+
if (!context?.notionToken) {
|
| 157 |
+
throw new Error("Notion Integration Token์ด ์์ต๋๋ค (ctx.notionToken).");
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
const databaseId = input.database_id || context.defaultDatabaseId;
|
| 161 |
+
const databaseName = input.database_name || context.defaultDatabaseName;
|
| 162 |
+
|
| 163 |
+
const notion = new Client({ auth: context.notionToken });
|
| 164 |
+
|
| 165 |
+
switch (input.operation) {
|
| 166 |
+
case "query": {
|
| 167 |
+
if (!databaseId) throw new Error("database_id๊ฐ ํ์ํฉ๋๋ค.");
|
| 168 |
+
const res = await notion.databases.query({
|
| 169 |
+
database_id: databaseId,
|
| 170 |
+
filter: input.query?.filter as any,
|
| 171 |
+
sorts: input.query?.sorts as any,
|
| 172 |
+
page_size: input.query?.page_size,
|
| 173 |
+
});
|
| 174 |
+
return { operation: input.operation, database_id: databaseId, database_name: databaseName, result: res };
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
case "create": {
|
| 178 |
+
if (!databaseId) throw new Error("database_id๊ฐ ํ์ํฉ๋๋ค.");
|
| 179 |
+
if (!input.properties) throw new Error("create์๋ properties๊ฐ ํ์ํฉ๋๋ค.");
|
| 180 |
+
const res = await notion.pages.create({
|
| 181 |
+
parent: { database_id: databaseId },
|
| 182 |
+
properties: input.properties as any,
|
| 183 |
+
});
|
| 184 |
+
return { operation: input.operation, database_id: databaseId, database_name: databaseName, result: res };
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
case "update": {
|
| 188 |
+
if (!input.page_id) throw new Error("update์๋ page_id๊ฐ ํ์ํฉ๋๋ค.");
|
| 189 |
+
if (!input.properties) throw new Error("update์๋ properties๊ฐ ํ์ํฉ๋๋ค.");
|
| 190 |
+
const res = await notion.pages.update({
|
| 191 |
+
page_id: input.page_id,
|
| 192 |
+
properties: input.properties as any,
|
| 193 |
+
});
|
| 194 |
+
return { operation: input.operation, database_id: databaseId, database_name: databaseName, result: res };
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
case "archive": {
|
| 198 |
+
if (!input.page_id) throw new Error("archive์๋ page_id๊ฐ ํ์ํฉ๋๋ค.");
|
| 199 |
+
const res = await notion.pages.update({
|
| 200 |
+
page_id: input.page_id,
|
| 201 |
+
archived: true,
|
| 202 |
+
} as any);
|
| 203 |
+
return { operation: input.operation, database_id: databaseId, database_name: databaseName, result: res };
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
case "upsert": {
|
| 207 |
+
if (!databaseId) throw new Error("database_id๊ฐ ํ์ํฉ๋๋ค.");
|
| 208 |
+
if (!input.upsert) throw new Error("upsert์๋ upsert ์ ๋ณด๊ฐ ํ์ํฉ๋๋ค.");
|
| 209 |
+
if (!input.properties) throw new Error("upsert์๋ properties(์
๋ฐ์ดํธ/์์ฑํ ๊ฐ)๊ฐ ํ์ํฉ๋๋ค.");
|
| 210 |
+
|
| 211 |
+
// TODO: unique_property_name/value ๊ธฐ๋ฐ์ผ๋ก DB์์ ๋จผ์ ๊ฒ์ โ ์์ผ๋ฉด update, ์์ผ๋ฉด create
|
| 212 |
+
// 1) query: filter๋ก unique property ๋งค์นญ
|
| 213 |
+
// 2) results[0] ์์ผ๋ฉด pages.update
|
| 214 |
+
// 3) ์์ผ๋ฉด pages.create
|
| 215 |
+
throw new Error("upsert๋ ์์ง ์ค์ผ๋ ํค์
๋๋ค. TODO ๊ตฌํ์ด ํ์ํฉ๋๋ค.");
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
},
|
| 219 |
+
};
|
| 220 |
+
|
| 221 |
+
|
app/core/logger.py
CHANGED
|
@@ -36,7 +36,18 @@ def get_logger(name: str, level: int = logging.INFO) -> logging.Logger:
|
|
| 36 |
logger.setLevel(level)
|
| 37 |
|
| 38 |
# ์ฝ์ ํธ๋ค๋ฌ
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
console_handler.setLevel(level)
|
| 41 |
console_formatter = logging.Formatter(LOG_FORMAT, datefmt=DATE_FORMAT)
|
| 42 |
console_handler.setFormatter(console_formatter)
|
|
|
|
| 36 |
logger.setLevel(level)
|
| 37 |
|
| 38 |
# ์ฝ์ ํธ๋ค๋ฌ
|
| 39 |
+
# Windows์์ cp949 ์ธ์ฝ๋ฉ์ผ๋ก ์ธํ ์ด๋ชจ์ง(\u26a0 ๋ฑ) ์ถ๋ ฅ ์ค๋ฅ ํด๊ฒฐ
|
| 40 |
+
stream = sys.stdout
|
| 41 |
+
if sys.platform == 'win32':
|
| 42 |
+
for s in [sys.stdout, sys.stderr]:
|
| 43 |
+
if hasattr(s, 'reconfigure'):
|
| 44 |
+
try:
|
| 45 |
+
# ์๋ฌ ๋ฐ์ ์ ๋์ฒด ๋ฌธ์๋ก ์นํํ์ฌ ๋ก๊ทธ ์ถ๋ ฅ ์ค๋จ ๋ฐฉ์ง
|
| 46 |
+
s.reconfigure(errors='replace')
|
| 47 |
+
except Exception:
|
| 48 |
+
pass
|
| 49 |
+
|
| 50 |
+
console_handler = logging.StreamHandler(stream)
|
| 51 |
console_handler.setLevel(level)
|
| 52 |
console_formatter = logging.Formatter(LOG_FORMAT, datefmt=DATE_FORMAT)
|
| 53 |
console_handler.setFormatter(console_formatter)
|
app/database.py
CHANGED
|
@@ -797,3 +797,20 @@ class WebtoonStageKeyMapping(db.Model):
|
|
| 797 |
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
| 798 |
}
|
| 799 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 797 |
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
| 798 |
}
|
| 799 |
|
| 800 |
+
# Notion ์ผ์ ํ ๋ฐ์ดํฐ ์บ์ ๋ชจ๋ธ
|
| 801 |
+
class NotionScheduleCache(db.Model):
|
| 802 |
+
__tablename__ = 'notion_schedule_cache'
|
| 803 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 804 |
+
notion_id = db.Column(db.String(100), unique=True, nullable=False, index=True)
|
| 805 |
+
name = db.Column(db.String(255), nullable=True, index=True) # ๋ด๋น์ ์ด๋ฆ (name_filter ๋์)
|
| 806 |
+
start_date = db.Column(db.String(20), nullable=True, index=True) # YYYY-MM-DD
|
| 807 |
+
end_date = db.Column(db.String(20), nullable=True, index=True) # YYYY-MM-DD
|
| 808 |
+
data_json = db.Column(db.Text, nullable=False) # ์ ์ฒด ๋ฐ์ดํฐ (properties ํฌํจ)
|
| 809 |
+
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
| 810 |
+
|
| 811 |
+
def to_dict(self):
|
| 812 |
+
try:
|
| 813 |
+
return json.loads(self.data_json)
|
| 814 |
+
except:
|
| 815 |
+
return {}
|
| 816 |
+
|
app/migrations.py
CHANGED
|
@@ -121,6 +121,16 @@ def check_and_migrate_db(app):
|
|
| 121 |
logger.error(f"webtoon_milestone_manager ๋ง์ด๊ทธ๋ ์ด์
์ค ์ค๋ฅ: {e}")
|
| 122 |
conn.rollback()
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
logger.info("๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์
์๋ฃ")
|
| 125 |
|
| 126 |
except Exception as e:
|
|
|
|
| 121 |
logger.error(f"webtoon_milestone_manager ๋ง์ด๊ทธ๋ ์ด์
์ค ์ค๋ฅ: {e}")
|
| 122 |
conn.rollback()
|
| 123 |
|
| 124 |
+
# 5. notion_schedule_cache ํ
์ด๋ธ ํ์ธ ๋ฐ ์์ฑ
|
| 125 |
+
if 'notion_schedule_cache' not in table_names:
|
| 126 |
+
logger.info("notion_schedule_cache ํ
์ด๋ธ์ด ์์ด ์์ฑํฉ๋๋ค.")
|
| 127 |
+
try:
|
| 128 |
+
# db.create_all()์ ๋ชจ๋ ๋๋ฝ๋ ํ
์ด๋ธ์ ์์ฑํ๋ฏ๋ก ์์ ํจ
|
| 129 |
+
db.create_all()
|
| 130 |
+
logger.info("notion_schedule_cache ํ
์ด๋ธ ์์ฑ ์๋ฃ")
|
| 131 |
+
except Exception as e:
|
| 132 |
+
logger.error(f"notion_schedule_cache ํ
์ด๋ธ ์์ฑ ์คํจ: {e}")
|
| 133 |
+
|
| 134 |
logger.info("๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์
์๋ฃ")
|
| 135 |
|
| 136 |
except Exception as e:
|
app/routes.py
CHANGED
|
@@ -14,7 +14,7 @@ from app.database import (
|
|
| 14 |
WebtoonWBSAnalysis, WebtoonWBSJob,
|
| 15 |
WebtoonProjectDuration, WebtoonEpisodeDuration, WebtoonDurationJob,
|
| 16 |
WebtoonStageKeyMapping, WebtoonMilestone, WebtoonMilestoneManager,
|
| 17 |
-
Notice, NoticeRecipient
|
| 18 |
)
|
| 19 |
from app.vector_db import get_vector_db
|
| 20 |
from app.gemini_client import get_gemini_client
|
|
@@ -8940,9 +8940,9 @@ def get_schedule():
|
|
| 8940 |
databases = []
|
| 8941 |
|
| 8942 |
schedule_db = None
|
| 8943 |
-
for
|
| 8944 |
-
if isinstance(
|
| 8945 |
-
schedule_db =
|
| 8946 |
break
|
| 8947 |
|
| 8948 |
if not schedule_db or not schedule_db.get('id'):
|
|
@@ -8998,13 +8998,13 @@ def get_schedule():
|
|
| 8998 |
if not date_property_name:
|
| 8999 |
print("[์ผ์ ํ ์กฐํ] ๊ฒฝ๊ณ : ๋ ์ง ํ๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. ํํฐ ์์ด ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.")
|
| 9000 |
|
| 9001 |
-
# ์ค๋ ๋ ์ง ๊ธฐ์ค ๋ฒ์ ๊ณ์ฐ (์ง๋ ์ฃผ ์์์ผ ~
|
| 9002 |
today = datetime.now().date()
|
| 9003 |
# ์ด๋ฒ ์ฃผ ์์์ผ ์ฐพ๊ธฐ (์์์ผ์ด 0)
|
| 9004 |
days_since_monday = today.weekday()
|
| 9005 |
this_week_start = today - timedelta(days=days_since_monday)
|
| 9006 |
week_start = this_week_start - timedelta(days=7) # ์ง๋ ์ฃผ ์์์ผ
|
| 9007 |
-
week_end = this_week_start + timedelta(days=
|
| 9008 |
|
| 9009 |
print(f"[์ผ์ ํ ์กฐํ] ๋ฒ์: {week_start.isoformat()} ~ {week_end.isoformat()}")
|
| 9010 |
|
|
@@ -9127,6 +9127,8 @@ def get_schedule():
|
|
| 9127 |
max_retries = 3
|
| 9128 |
last_exception = None
|
| 9129 |
response = None
|
|
|
|
|
|
|
| 9130 |
|
| 9131 |
for attempt in range(max_retries):
|
| 9132 |
try:
|
|
@@ -9136,7 +9138,10 @@ def get_schedule():
|
|
| 9136 |
json=query_data,
|
| 9137 |
timeout=30 # ํ์์์ 30์ด
|
| 9138 |
)
|
| 9139 |
-
|
|
|
|
|
|
|
|
|
|
| 9140 |
except (requests.exceptions.ReadTimeout, requests.exceptions.RequestException) as e:
|
| 9141 |
last_exception = e
|
| 9142 |
print(f"[Notion API] ์๋ {attempt+1}/{max_retries} ์คํจ: {e}")
|
|
@@ -9147,124 +9152,193 @@ def get_schedule():
|
|
| 9147 |
last_exception = e
|
| 9148 |
break # ์์์น ๋ชปํ ์ค๋ฅ๋ ์ฆ์ ์ค๋จ
|
| 9149 |
|
| 9150 |
-
|
| 9151 |
-
|
| 9152 |
-
print(f"[์ผ์ ํ ์กฐํ] {error_msg}")
|
| 9153 |
-
return jsonify({'error': error_msg}), 504
|
| 9154 |
-
|
| 9155 |
-
if response.status_code != 200:
|
| 9156 |
-
error_data = response.json()
|
| 9157 |
-
return jsonify({
|
| 9158 |
-
'error': f"Notion API ์ค๋ฅ ({response.status_code}): {error_data.get('message', '์ ์ ์๋ ์ค๋ฅ')}"
|
| 9159 |
-
}), response.status_code
|
| 9160 |
|
| 9161 |
-
|
| 9162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9163 |
|
| 9164 |
# ๊ฒฐ๊ณผ ๊ฐ๊ณต
|
| 9165 |
processed_results = []
|
| 9166 |
# date_property_name์ ์์์ ์คํค๋ง๋ฅผ ํตํด ์ด๋ฏธ ๊ฒฐ์ ๋จ
|
| 9167 |
|
| 9168 |
for page in all_results:
|
| 9169 |
-
|
| 9170 |
-
|
| 9171 |
-
|
| 9172 |
-
|
| 9173 |
-
'properties'
|
| 9174 |
-
|
| 9175 |
-
|
| 9176 |
-
|
| 9177 |
-
|
| 9178 |
-
|
| 9179 |
-
|
| 9180 |
|
| 9181 |
-
|
| 9182 |
-
|
| 9183 |
-
|
| 9184 |
-
|
| 9185 |
-
|
| 9186 |
-
|
| 9187 |
-
|
| 9188 |
-
|
| 9189 |
-
|
| 9190 |
-
|
| 9191 |
-
|
| 9192 |
-
|
| 9193 |
-
|
| 9194 |
-
|
| 9195 |
-
|
| 9196 |
-
|
| 9197 |
-
# ๊ธฐ์กด ํธํ์ฑ์ ์ํด val์ start ๊ฐ ์ ์ง
|
| 9198 |
-
val = start_val
|
| 9199 |
-
|
| 9200 |
-
# Gantt ์ฐจํธ ๋ฑ์ ์ํด ์์ธ ์ ๋ณด ์ ์ฅ
|
| 9201 |
-
page_data['properties'][f"{prop_name}_detail"] = {
|
| 9202 |
-
'start': start_val,
|
| 9203 |
-
'end': end_val or start_val
|
| 9204 |
-
}
|
| 9205 |
-
|
| 9206 |
-
# ๋ ์ง ํ๋ ๋ฐ๊ฒฌ ์ ์ ์ฅ (์คํค๋ง์์ ๋ชป ์ฐพ์์ ๊ฒฝ์ฐ ๋๋น)
|
| 9207 |
-
if not date_property_name:
|
| 9208 |
-
date_property_name = prop_name
|
| 9209 |
-
|
| 9210 |
-
# ์ ๋ ฌ์ฉ ๋ ์ง ๊ฐ ์ ์ฅ (ํ์ฌ ์ฒ๋ฆฌ ์ค์ธ prop_name์ด date_property_name์ธ ๊ฒฝ์ฐ์๋ง ์ฐ์ ์ ์ผ๋ก ์ฌ์ฉ)
|
| 9211 |
-
if start_val:
|
| 9212 |
-
try:
|
| 9213 |
-
from datetime import datetime
|
| 9214 |
-
current_date_val = datetime.fromisoformat(start_val.replace('Z', '+00:00'))
|
| 9215 |
-
# date_property_name ํ๋๋ฉด ๋ฌด์กฐ๊ฑด ์ ์ฅ, ์๋๋ฉด ๊ธฐ์กด์ ์์ ๋๋ง ์ ์ฅ
|
| 9216 |
-
if prop_name == date_property_name or page_data['date_value'] is None:
|
| 9217 |
-
page_data['date_value'] = current_date_val
|
| 9218 |
-
except:
|
| 9219 |
-
pass
|
| 9220 |
-
elif ptype == 'formula':
|
| 9221 |
-
formula_val = prop_val.get('formula', {})
|
| 9222 |
-
ftype = formula_val.get('type')
|
| 9223 |
-
if ftype == 'date':
|
| 9224 |
-
date_obj = formula_val.get('date')
|
| 9225 |
if date_obj:
|
| 9226 |
start_val = date_obj.get('start')
|
| 9227 |
end_val = date_obj.get('end')
|
| 9228 |
-
val = start_val
|
| 9229 |
|
|
|
|
| 9230 |
page_data['properties'][f"{prop_name}_detail"] = {
|
| 9231 |
'start': start_val,
|
| 9232 |
'end': end_val or start_val
|
| 9233 |
}
|
| 9234 |
|
|
|
|
|
|
|
|
|
|
| 9235 |
if start_val:
|
| 9236 |
try:
|
| 9237 |
-
from datetime import datetime
|
| 9238 |
current_date_val = datetime.fromisoformat(start_val.replace('Z', '+00:00'))
|
| 9239 |
if prop_name == date_property_name or page_data['date_value'] is None:
|
| 9240 |
page_data['date_value'] = current_date_val
|
| 9241 |
except:
|
| 9242 |
pass
|
| 9243 |
-
elif
|
| 9244 |
-
|
| 9245 |
-
|
| 9246 |
-
|
| 9247 |
-
|
| 9248 |
-
|
| 9249 |
-
|
| 9250 |
-
|
| 9251 |
-
|
| 9252 |
-
|
| 9253 |
-
|
| 9254 |
-
|
| 9255 |
-
|
| 9256 |
-
|
| 9257 |
-
|
| 9258 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9259 |
|
| 9260 |
-
page_data
|
| 9261 |
-
|
| 9262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9263 |
|
| 9264 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9265 |
if date_property_name:
|
| 9266 |
filtered_results = []
|
| 9267 |
for item in processed_results:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9268 |
date_str = item['properties'].get(date_property_name)
|
| 9269 |
if date_str:
|
| 9270 |
try:
|
|
@@ -9272,30 +9346,20 @@ def get_schedule():
|
|
| 9272 |
if week_start <= item_date <= week_end:
|
| 9273 |
filtered_results.append(item)
|
| 9274 |
except:
|
| 9275 |
-
# ๋ ์ง ํ์ฑ ์คํจ ์ ํฌํจํ์ง ์์
|
| 9276 |
pass
|
| 9277 |
-
else:
|
| 9278 |
-
# ๋ ์ง๊ฐ ์๋ ํญ๋ชฉ์ ์ ์ธ
|
| 9279 |
-
pass
|
| 9280 |
|
| 9281 |
-
# filtered_results๊ฐ ๋น์ด์๋๋ผ๋ ์
๋ฐ์ดํธํ์ฌ ๋น ๋ชฉ๋ก์ ๋ฐํํจ
|
| 9282 |
processed_results = filtered_results
|
| 9283 |
if filtered_results:
|
| 9284 |
print(f"[์ผ์ ํ ์กฐํ] ์๋ฒ ์ธก ํํฐ๋ง ํ: {len(processed_results)}๊ฐ ํญ๋ชฉ")
|
| 9285 |
else:
|
| 9286 |
-
print(f"[์ผ์ ํ ์กฐํ] ๊ฒฝ๊ณ : ์ด๋ฒ ์ฃผ ๋ฒ์์ ํด๋นํ๋ ํญ๋ชฉ์ด ์์ต๋๋ค.
|
| 9287 |
|
| 9288 |
# ๋ ์ง ๊ธฐ์ค์ผ๋ก ์ ๋ ฌ (์ต๊ทผ ๋ ์ง๊ฐ ๋จผ์ )
|
| 9289 |
-
|
| 9290 |
-
processed_results.sort(key=lambda x: (x['date_value'] is None, x['date_value'] or datetime.min), reverse=True)
|
| 9291 |
|
| 9292 |
# has_more ๊ฐ ํ์ธ ๋ฐ ๋ก๊ทธ ์ถ๋ ฅ
|
| 9293 |
has_more_value = data.get('has_more', False)
|
| 9294 |
next_cursor_value = data.get('next_cursor')
|
| 9295 |
-
print(f"[์ผ์ ํ ์กฐํ] has_more ๊ฐ: {has_more_value} (ํ์
: {type(has_more_value).__name__})")
|
| 9296 |
-
print(f"[์ผ์ ํ ์กฐํ] next_cursor ๊ฐ: {next_cursor_value}")
|
| 9297 |
-
print(f"[์ผ์ ํ ์กฐํ] Notion API ์๋ต์ results ๊ฐ์: {len(data.get('results', []))}")
|
| 9298 |
-
print(f"[์ผ์ ํ ์กฐํ] ์ฒ๋ฆฌ๋ results ๊ฐ์: {len(processed_results)}")
|
| 9299 |
|
| 9300 |
filter_desc = []
|
| 9301 |
if date_property_name:
|
|
@@ -9311,6 +9375,8 @@ def get_schedule():
|
|
| 9311 |
'date_property': date_property_name,
|
| 9312 |
'has_more': has_more_value,
|
| 9313 |
'next_cursor': next_cursor_value,
|
|
|
|
|
|
|
| 9314 |
'query_info': {
|
| 9315 |
'filter': " & ".join(filter_desc) if filter_desc else '๋ชจ๋ ๋ฐ์ดํฐ (ํํฐ ์์)',
|
| 9316 |
'sort': f"'{date_property_name}' ํ๋ ๊ธฐ์ค ๋ด๋ฆผ์ฐจ์ (์ต์ ์)" if date_property_name else '์ ๋ ฌ ํ๋ ์์'
|
|
@@ -9351,13 +9417,13 @@ def update_notion_db_aliases():
|
|
| 9351 |
# ๋ณ์นญ ์
๋ฐ์ดํธ
|
| 9352 |
alias_map = {a['id']: a.get('alias') for a in aliases if isinstance(a, dict) and a.get('id')}
|
| 9353 |
|
| 9354 |
-
for
|
| 9355 |
-
if isinstance(
|
| 9356 |
-
alias_value = alias_map[
|
| 9357 |
if alias_value:
|
| 9358 |
-
|
| 9359 |
-
elif 'alias' in
|
| 9360 |
-
del
|
| 9361 |
|
| 9362 |
# ์ ์ฅ
|
| 9363 |
SystemConfig.set_config(
|
|
|
|
| 14 |
WebtoonWBSAnalysis, WebtoonWBSJob,
|
| 15 |
WebtoonProjectDuration, WebtoonEpisodeDuration, WebtoonDurationJob,
|
| 16 |
WebtoonStageKeyMapping, WebtoonMilestone, WebtoonMilestoneManager,
|
| 17 |
+
Notice, NoticeRecipient, NotionScheduleCache
|
| 18 |
)
|
| 19 |
from app.vector_db import get_vector_db
|
| 20 |
from app.gemini_client import get_gemini_client
|
|
|
|
| 8940 |
databases = []
|
| 8941 |
|
| 8942 |
schedule_db = None
|
| 8943 |
+
for db_info in databases:
|
| 8944 |
+
if isinstance(db_info, dict) and db_info.get('alias') == '์ผ์ ํ':
|
| 8945 |
+
schedule_db = db_info
|
| 8946 |
break
|
| 8947 |
|
| 8948 |
if not schedule_db or not schedule_db.get('id'):
|
|
|
|
| 8998 |
if not date_property_name:
|
| 8999 |
print("[์ผ์ ํ ์กฐํ] ๊ฒฝ๊ณ : ๋ ์ง ํ๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. ํํฐ ์์ด ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.")
|
| 9000 |
|
| 9001 |
+
# ์ค๋ ๋ ์ง ๊ธฐ์ค ๋ฒ์ ๊ณ์ฐ (์ง๋ ์ฃผ ์์์ผ ~ ๋ค์ ์ฃผ ์ผ์์ผ)
|
| 9002 |
today = datetime.now().date()
|
| 9003 |
# ์ด๋ฒ ์ฃผ ์์์ผ ์ฐพ๊ธฐ (์์์ผ์ด 0)
|
| 9004 |
days_since_monday = today.weekday()
|
| 9005 |
this_week_start = today - timedelta(days=days_since_monday)
|
| 9006 |
week_start = this_week_start - timedelta(days=7) # ์ง๋ ์ฃผ ์์์ผ
|
| 9007 |
+
week_end = this_week_start + timedelta(days=13) # ๋ค์ ์ฃผ ์ผ์์ผ
|
| 9008 |
|
| 9009 |
print(f"[์ผ์ ํ ์กฐํ] ๋ฒ์: {week_start.isoformat()} ~ {week_end.isoformat()}")
|
| 9010 |
|
|
|
|
| 9127 |
max_retries = 3
|
| 9128 |
last_exception = None
|
| 9129 |
response = None
|
| 9130 |
+
is_from_cache = False
|
| 9131 |
+
error_message = None
|
| 9132 |
|
| 9133 |
for attempt in range(max_retries):
|
| 9134 |
try:
|
|
|
|
| 9138 |
json=query_data,
|
| 9139 |
timeout=30 # ํ์์์ 30์ด
|
| 9140 |
)
|
| 9141 |
+
if response.status_code == 200:
|
| 9142 |
+
break # ์ฑ๊ณต ์ ๋ฃจํ ํ์ถ
|
| 9143 |
+
else:
|
| 9144 |
+
last_exception = Exception(f"API Error {response.status_code}: {response.text}")
|
| 9145 |
except (requests.exceptions.ReadTimeout, requests.exceptions.RequestException) as e:
|
| 9146 |
last_exception = e
|
| 9147 |
print(f"[Notion API] ์๋ {attempt+1}/{max_retries} ์คํจ: {e}")
|
|
|
|
| 9152 |
last_exception = e
|
| 9153 |
break # ์์์น ๋ชปํ ์ค๋ฅ๋ ์ฆ์ ์ค๋จ
|
| 9154 |
|
| 9155 |
+
data = None
|
| 9156 |
+
all_results = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9157 |
|
| 9158 |
+
if response is not None and response.status_code == 200:
|
| 9159 |
+
data = response.json()
|
| 9160 |
+
all_results.extend(data.get('results', []))
|
| 9161 |
+
else:
|
| 9162 |
+
# Notion API ์คํจ ์ ์บ์๋ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
|
| 9163 |
+
print(f"[์ผ์ ํ ์กฐํ] Notion API ํธ์ถ ์คํจ. ์บ์๋ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํฉ๋๋ค. (์ค๋ฅ: {last_exception})")
|
| 9164 |
+
error_message = f"Notion API ์ฐ๊ฒฐ์ ์คํจํ์ต๋๋ค. ์ ์ฅ๋ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ต๋๋ค. (์ค๋ฅ: {str(last_exception)})"
|
| 9165 |
+
is_from_cache = True
|
| 9166 |
+
|
| 9167 |
+
# ์บ์๋ ๋ชจ๋ ๋ฐ์ดํฐ ์กฐํ (๋ฉ๋ชจ๋ฆฌ์์ ๋ ์ง ํํฐ๋ง)
|
| 9168 |
+
cache_items = NotionScheduleCache.query.all()
|
| 9169 |
+
for item in cache_items:
|
| 9170 |
+
all_results.append(item.to_dict())
|
| 9171 |
+
|
| 9172 |
+
# ์บ์ ๋ฐ์ดํฐ์ ๊ฒฝ์ฐ 'data' ๊ฐ์ฒด ๋ชจ์กฐ ์์ฑ
|
| 9173 |
+
data = {
|
| 9174 |
+
'results': all_results,
|
| 9175 |
+
'has_more': False,
|
| 9176 |
+
'next_cursor': None
|
| 9177 |
+
}
|
| 9178 |
|
| 9179 |
# ๊ฒฐ๊ณผ ๊ฐ๊ณต
|
| 9180 |
processed_results = []
|
| 9181 |
# date_property_name์ ์์์ ์คํค๋ง๋ฅผ ํตํด ์ด๋ฏธ ๊ฒฐ์ ๋จ
|
| 9182 |
|
| 9183 |
for page in all_results:
|
| 9184 |
+
# ์บ์ ๋ฐ์ดํฐ(์ด๋ฏธ ๊ฐ๊ณต๋จ)์ ๋
ธ์
์๋ณธ ๋ฐ์ดํฐ(๊ฐ๊ณต ํ์) ๊ตฌ๋ถ
|
| 9185 |
+
# ๋
ธ์
์๋ณธ ๋ฐ์ดํฐ๋ 'object' ํ๋๊ฐ 'page'์
๋๋ค.
|
| 9186 |
+
if page.get('object') == 'page':
|
| 9187 |
+
# ๋
ธ์
์๋ณธ ๋ฐ์ดํฐ ๊ฐ๊ณต ์์
|
| 9188 |
+
properties = page.get('properties', {})
|
| 9189 |
+
page_data = {
|
| 9190 |
+
'id': page.get('id'),
|
| 9191 |
+
'url': page.get('url'),
|
| 9192 |
+
'properties': {},
|
| 9193 |
+
'date_value': None # ์ ๋ ฌ์ฉ ๋ ์ง ๊ฐ
|
| 9194 |
+
}
|
| 9195 |
|
| 9196 |
+
for prop_name, prop_val in properties.items():
|
| 9197 |
+
ptype = prop_val.get('type')
|
| 9198 |
+
val = None
|
| 9199 |
+
|
| 9200 |
+
if ptype == 'title':
|
| 9201 |
+
val = "".join([t.get('plain_text', '') for t in prop_val.get('title', [])])
|
| 9202 |
+
elif ptype == 'rich_text':
|
| 9203 |
+
val = "".join([t.get('plain_text', '') for t in prop_val.get('rich_text', [])])
|
| 9204 |
+
elif ptype == 'select':
|
| 9205 |
+
val = prop_val.get('select', {}).get('name') if prop_val.get('select') else None
|
| 9206 |
+
elif ptype == 'multi_select':
|
| 9207 |
+
val = [m.get('name') for m in prop_val.get('multi_select', [])]
|
| 9208 |
+
elif ptype == 'number':
|
| 9209 |
+
val = prop_val.get('number')
|
| 9210 |
+
elif ptype == 'date':
|
| 9211 |
+
date_obj = prop_val.get('date')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9212 |
if date_obj:
|
| 9213 |
start_val = date_obj.get('start')
|
| 9214 |
end_val = date_obj.get('end')
|
|
|
|
| 9215 |
|
| 9216 |
+
val = start_val
|
| 9217 |
page_data['properties'][f"{prop_name}_detail"] = {
|
| 9218 |
'start': start_val,
|
| 9219 |
'end': end_val or start_val
|
| 9220 |
}
|
| 9221 |
|
| 9222 |
+
if not date_property_name:
|
| 9223 |
+
date_property_name = prop_name
|
| 9224 |
+
|
| 9225 |
if start_val:
|
| 9226 |
try:
|
|
|
|
| 9227 |
current_date_val = datetime.fromisoformat(start_val.replace('Z', '+00:00'))
|
| 9228 |
if prop_name == date_property_name or page_data['date_value'] is None:
|
| 9229 |
page_data['date_value'] = current_date_val
|
| 9230 |
except:
|
| 9231 |
pass
|
| 9232 |
+
elif ptype == 'formula':
|
| 9233 |
+
formula_val = prop_val.get('formula', {})
|
| 9234 |
+
ftype = formula_val.get('type')
|
| 9235 |
+
if ftype == 'date':
|
| 9236 |
+
date_obj = formula_val.get('date')
|
| 9237 |
+
if date_obj:
|
| 9238 |
+
start_val = date_obj.get('start')
|
| 9239 |
+
end_val = date_obj.get('end')
|
| 9240 |
+
val = start_val
|
| 9241 |
+
|
| 9242 |
+
page_data['properties'][f"{prop_name}_detail"] = {
|
| 9243 |
+
'start': start_val,
|
| 9244 |
+
'end': end_val or start_val
|
| 9245 |
+
}
|
| 9246 |
+
|
| 9247 |
+
if start_val:
|
| 9248 |
+
try:
|
| 9249 |
+
current_date_val = datetime.fromisoformat(start_val.replace('Z', '+00:00'))
|
| 9250 |
+
if prop_name == date_property_name or page_data['date_value'] is None:
|
| 9251 |
+
page_data['date_value'] = current_date_val
|
| 9252 |
+
except:
|
| 9253 |
+
pass
|
| 9254 |
+
elif ftype == 'string':
|
| 9255 |
+
val = formula_val.get('string')
|
| 9256 |
+
elif ftype == 'number':
|
| 9257 |
+
val = formula_val.get('number')
|
| 9258 |
+
elif ftype == 'boolean':
|
| 9259 |
+
val = formula_val.get('boolean')
|
| 9260 |
+
elif ptype == 'checkbox':
|
| 9261 |
+
val = prop_val.get('checkbox')
|
| 9262 |
+
elif ptype == 'url':
|
| 9263 |
+
val = prop_val.get('url')
|
| 9264 |
+
elif ptype == 'email':
|
| 9265 |
+
val = prop_val.get('email')
|
| 9266 |
+
elif ptype == 'phone_number':
|
| 9267 |
+
val = prop_val.get('phone_number')
|
| 9268 |
+
else:
|
| 9269 |
+
val = f"({ptype})"
|
| 9270 |
+
|
| 9271 |
+
page_data['properties'][prop_name] = val
|
| 9272 |
|
| 9273 |
+
processed_results.append(page_data)
|
| 9274 |
+
else:
|
| 9275 |
+
# ์ด๋ฏธ ๊ฐ๊ณต๋ ๋ฐ์ดํฐ (์บ์์์ ์จ ๊ฒฝ์ฐ)
|
| 9276 |
+
# date_value๊ฐ datetime ๊ฐ์ฒด๊ฐ ์๋ ์ ์์ผ๋ฏ๋ก ๋ณต์
|
| 9277 |
+
if page.get('date_value') and isinstance(page['date_value'], str):
|
| 9278 |
+
try:
|
| 9279 |
+
page['date_value'] = datetime.fromisoformat(page['date_value'].replace('Z', '+00:00'))
|
| 9280 |
+
except:
|
| 9281 |
+
page['date_value'] = None
|
| 9282 |
+
processed_results.append(page)
|
| 9283 |
|
| 9284 |
+
# Notion์์ ์ฑ๊ณต์ ์ผ๋ก ๊ฐ์ ธ์จ ๊ฒฝ์ฐ ์บ์์ ์ ์ฅ/์
๋ฐ์ดํธ
|
| 9285 |
+
if not is_from_cache:
|
| 9286 |
+
try:
|
| 9287 |
+
for page_data in processed_results:
|
| 9288 |
+
notion_id = page_data.get('id')
|
| 9289 |
+
# '์ด๋ฆ' ์์ฑ์์ ์ด๋ฆ ์ถ์ถ (๋ด๋น์ ํํฐ๋ง์ฉ)
|
| 9290 |
+
name = page_data['properties'].get('์ด๋ฆ', '-')
|
| 9291 |
+
|
| 9292 |
+
# ๋ ์ง ์ถ์ถ
|
| 9293 |
+
start_date_s = None
|
| 9294 |
+
end_date_s = None
|
| 9295 |
+
if date_property_name:
|
| 9296 |
+
detail = page_data['properties'].get(f"{date_property_name}_detail", {})
|
| 9297 |
+
start_date_s = detail.get('start')
|
| 9298 |
+
end_date_s = detail.get('end') or start_date_s
|
| 9299 |
+
|
| 9300 |
+
# date_value๋ JSON ์ ์ฅ์ ์ํด ๋ฌธ์์ด๋ก ์์ ๋ณํ
|
| 9301 |
+
save_data = page_data.copy()
|
| 9302 |
+
if save_data.get('date_value') and isinstance(save_data['date_value'], datetime):
|
| 9303 |
+
save_data['date_value'] = save_data['date_value'].isoformat()
|
| 9304 |
+
|
| 9305 |
+
new_json = json.dumps(save_data, ensure_ascii=False)
|
| 9306 |
+
|
| 9307 |
+
cached_item = NotionScheduleCache.query.filter_by(notion_id=notion_id).first()
|
| 9308 |
+
if not cached_item:
|
| 9309 |
+
cached_item = NotionScheduleCache(
|
| 9310 |
+
notion_id=notion_id,
|
| 9311 |
+
name=name,
|
| 9312 |
+
start_date=start_date_s,
|
| 9313 |
+
end_date=end_date_s,
|
| 9314 |
+
data_json=new_json
|
| 9315 |
+
)
|
| 9316 |
+
db.session.add(cached_item)
|
| 9317 |
+
else:
|
| 9318 |
+
# ์์ ์ฌํญ์ด ์๋ ๊ฒฝ์ฐ์๋ง ์
๋ฐ์ดํธ
|
| 9319 |
+
if cached_item.data_json != new_json:
|
| 9320 |
+
cached_item.name = name
|
| 9321 |
+
cached_item.start_date = start_date_s
|
| 9322 |
+
cached_item.end_date = end_date_s
|
| 9323 |
+
cached_item.data_json = new_json
|
| 9324 |
+
|
| 9325 |
+
db.session.commit()
|
| 9326 |
+
print(f"[์ผ์ ํ ์กฐํ] {len(processed_results)}๊ฐ ํญ๋ชฉ ์บ์ ๋๊ธฐํ ์๋ฃ")
|
| 9327 |
+
except Exception as cache_err:
|
| 9328 |
+
db.session.rollback()
|
| 9329 |
+
print(f"[์ผ์ ํ ์กฐํ] ์บ์ ์ ์ฅ ์ค ์ค๋ฅ: {cache_err}")
|
| 9330 |
+
|
| 9331 |
+
# ์๋ฒ ์ธก ํํฐ๋ง (์บ์ ๋ฐ์ดํฐ ํฌํจ)
|
| 9332 |
if date_property_name:
|
| 9333 |
filtered_results = []
|
| 9334 |
for item in processed_results:
|
| 9335 |
+
# ์ด๋ฆ ํํฐ ์ ์ฉ (์บ์์์ ๊ฐ์ ธ์จ ๊ฒฝ์ฐ Notion API ํํฐ๊ฐ ์๋จนํ์ ์ ์์)
|
| 9336 |
+
if name_filter_vals:
|
| 9337 |
+
item_name = str(item['properties'].get('์ด๋ฆ', '') or '')
|
| 9338 |
+
if not any(val in item_name for val in name_filter_vals):
|
| 9339 |
+
continue
|
| 9340 |
+
|
| 9341 |
+
# ๋ ์ง ๋ฒ์ ํํฐ
|
| 9342 |
date_str = item['properties'].get(date_property_name)
|
| 9343 |
if date_str:
|
| 9344 |
try:
|
|
|
|
| 9346 |
if week_start <= item_date <= week_end:
|
| 9347 |
filtered_results.append(item)
|
| 9348 |
except:
|
|
|
|
| 9349 |
pass
|
|
|
|
|
|
|
|
|
|
| 9350 |
|
|
|
|
| 9351 |
processed_results = filtered_results
|
| 9352 |
if filtered_results:
|
| 9353 |
print(f"[์ผ์ ํ ์กฐํ] ์๋ฒ ์ธก ํํฐ๋ง ํ: {len(processed_results)}๊ฐ ํญ๋ชฉ")
|
| 9354 |
else:
|
| 9355 |
+
print(f"[์ผ์ ํ ์กฐํ] ๊ฒฝ๊ณ : ์ด๋ฒ ์ฃผ ๋ฒ์์ ํด๋นํ๋ ํญ๋ชฉ์ด ์์ต๋๋ค.")
|
| 9356 |
|
| 9357 |
# ๋ ์ง ๊ธฐ์ค์ผ๋ก ์ ๋ ฌ (์ต๊ทผ ๋ ์ง๊ฐ ๋จผ์ )
|
| 9358 |
+
processed_results.sort(key=lambda x: (x.get('date_value') is None, x.get('date_value') or datetime.min), reverse=True)
|
|
|
|
| 9359 |
|
| 9360 |
# has_more ๊ฐ ํ์ธ ๋ฐ ๋ก๊ทธ ์ถ๋ ฅ
|
| 9361 |
has_more_value = data.get('has_more', False)
|
| 9362 |
next_cursor_value = data.get('next_cursor')
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9363 |
|
| 9364 |
filter_desc = []
|
| 9365 |
if date_property_name:
|
|
|
|
| 9375 |
'date_property': date_property_name,
|
| 9376 |
'has_more': has_more_value,
|
| 9377 |
'next_cursor': next_cursor_value,
|
| 9378 |
+
'is_from_cache': is_from_cache,
|
| 9379 |
+
'error': error_message,
|
| 9380 |
'query_info': {
|
| 9381 |
'filter': " & ".join(filter_desc) if filter_desc else '๋ชจ๋ ๋ฐ์ดํฐ (ํํฐ ์์)',
|
| 9382 |
'sort': f"'{date_property_name}' ํ๋ ๊ธฐ์ค ๋ด๋ฆผ์ฐจ์ (์ต์ ์)" if date_property_name else '์ ๋ ฌ ํ๋ ์์'
|
|
|
|
| 9417 |
# ๋ณ์นญ ์
๋ฐ์ดํธ
|
| 9418 |
alias_map = {a['id']: a.get('alias') for a in aliases if isinstance(a, dict) and a.get('id')}
|
| 9419 |
|
| 9420 |
+
for db_info in databases:
|
| 9421 |
+
if isinstance(db_info, dict) and db_info.get('id') in alias_map:
|
| 9422 |
+
alias_value = alias_map[db_info['id']]
|
| 9423 |
if alias_value:
|
| 9424 |
+
db_info['alias'] = alias_value
|
| 9425 |
+
elif 'alias' in db_info:
|
| 9426 |
+
del db_info['alias'] # ๋น ๋ณ์นญ์ ์ ๊ฑฐ
|
| 9427 |
|
| 9428 |
# ์ ์ฅ
|
| 9429 |
SystemConfig.set_config(
|
check_has_more.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
์ผ์ ํ API์ has_more ๊ฐ์ ํ์ธํ๋ ์คํฌ๋ฆฝํธ
|
| 3 |
+
"""
|
| 4 |
+
import requests
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
# ์๋ฒ URL
|
| 8 |
+
base_url = "http://localhost:5001"
|
| 9 |
+
|
| 10 |
+
# ์ธ์
์์ฑ (์ฟ ํค ์ ์ง์ฉ)
|
| 11 |
+
session = requests.Session()
|
| 12 |
+
|
| 13 |
+
# ๋ก๊ทธ์ธ (ํ์ํ ๊ฒฝ์ฐ)
|
| 14 |
+
# ์ค์ ์ฌ์ฉ์๋ช
๊ณผ ๋น๋ฐ๋ฒํธ๋ก ๋ก๊ทธ์ธํด์ผ ํฉ๋๋ค
|
| 15 |
+
print("=" * 80)
|
| 16 |
+
print("์ผ์ ํ API์ has_more ๊ฐ ํ์ธ")
|
| 17 |
+
print("=" * 80)
|
| 18 |
+
print("\nโ ๏ธ ์ด ์คํฌ๋ฆฝํธ๋ ๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค.")
|
| 19 |
+
print("๋ธ๋ผ์ฐ์ ์์ ๋ก๊ทธ์ธํ ํ ์ฟ ํค๋ฅผ ์ฌ์ฉํ๊ฑฐ๋,")
|
| 20 |
+
print("์ง์ ๋ธ๋ผ์ฐ์ ๊ฐ๋ฐ์ ๋๊ตฌ์ Network ํญ์์ ํ์ธํ์ธ์.\n")
|
| 21 |
+
|
| 22 |
+
# API ์๋ํฌ์ธํธ
|
| 23 |
+
api_url = f"{base_url}/api/admin/webtoon/agent/schedule"
|
| 24 |
+
|
| 25 |
+
print(f"API URL: {api_url}")
|
| 26 |
+
print("\n๋ธ๋ผ์ฐ์ ์์ ๋ค์ ๋จ๊ณ๋ฅผ ๋ฐ๋ผ์ฃผ์ธ์:")
|
| 27 |
+
print("1. http://localhost:5001/admin/agent/schedule ํ์ด์ง๋ฅผ ์ฝ๋๋ค")
|
| 28 |
+
print("2. ๋ก๊ทธ์ธํฉ๋๋ค (ํ์ํ ๊ฒฝ์ฐ)")
|
| 29 |
+
print("3. '์๋ก๊ณ ์นจ' ๋ฒํผ์ ํด๋ฆญํฉ๋๋ค")
|
| 30 |
+
print("4. ๋ธ๋ผ์ฐ์ ๊ฐ๋ฐ์ ๋๊ตฌ(F12)๋ฅผ ์ฝ๋๋ค")
|
| 31 |
+
print("5. Network ํญ์์ '/api/admin/webtoon/agent/schedule' ์์ฒญ์ ์ฐพ์ต๋๋ค")
|
| 32 |
+
print("6. Response ํญ์์ 'has_more' ๊ฐ์ ํ์ธํฉ๋๋ค")
|
| 33 |
+
print("\n๋๋ ์ฝ์์์ ๋ค์ ๋ช
๋ น์ ์คํํ์ธ์:")
|
| 34 |
+
print("fetch('/api/admin/webtoon/agent/schedule').then(r => r.json()).then(d => console.log('has_more:', d.has_more))")
|
| 35 |
+
|
check_notion_token.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import sys
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
# ํ๋ก์ ํธ ๋ฃจํธ๋ฅผ path์ ์ถ๊ฐ
|
| 7 |
+
project_root = Path(__file__).parent
|
| 8 |
+
sys.path.append(str(project_root))
|
| 9 |
+
|
| 10 |
+
from app import create_app
|
| 11 |
+
from app.database import db, SystemConfig
|
| 12 |
+
|
| 13 |
+
def check_notion_config():
|
| 14 |
+
app = create_app()
|
| 15 |
+
with app.app_context():
|
| 16 |
+
keys = ['notion_integration_token', 'notion_database_id', 'notion_database_name']
|
| 17 |
+
print("-" * 50)
|
| 18 |
+
print("Notion ์ค์ ๊ฐ ์์ธ ํ์ธ")
|
| 19 |
+
print("-" * 50)
|
| 20 |
+
|
| 21 |
+
for key in keys:
|
| 22 |
+
config = SystemConfig.query.filter_by(key=key).first()
|
| 23 |
+
if config:
|
| 24 |
+
val = config.value
|
| 25 |
+
print(f"[{key}]")
|
| 26 |
+
print(f" - ๊ฐ: {val}")
|
| 27 |
+
print(f" - ๊ธธ์ด: {len(val)}์")
|
| 28 |
+
else:
|
| 29 |
+
print(f"[{key}] ์ค์ ๋์ง ์์")
|
| 30 |
+
print("-" * 50)
|
| 31 |
+
|
| 32 |
+
if __name__ == "__main__":
|
| 33 |
+
check_notion_config()
|
debug_dbs.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import sqlite3
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
dbs = ['instance/finance_analysis.db', 'instance/soy_nv_ai.db', 'instance/database.db']
|
| 6 |
+
|
| 7 |
+
for db_path in dbs:
|
| 8 |
+
if not os.path.exists(db_path):
|
| 9 |
+
print(f"File not found: {db_path}")
|
| 10 |
+
continue
|
| 11 |
+
|
| 12 |
+
print(f"\n--- Checking {db_path} ---")
|
| 13 |
+
try:
|
| 14 |
+
conn = sqlite3.connect(db_path)
|
| 15 |
+
cursor = conn.cursor()
|
| 16 |
+
|
| 17 |
+
# ํ
์ด๋ธ ์กด์ฌ ํ์ธ
|
| 18 |
+
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='system_config'")
|
| 19 |
+
if not cursor.fetchone():
|
| 20 |
+
print("Table 'system_config' not found.")
|
| 21 |
+
conn.close()
|
| 22 |
+
continue
|
| 23 |
+
|
| 24 |
+
cursor.execute("SELECT key, value FROM system_config WHERE key LIKE 'notion_%'")
|
| 25 |
+
rows = cursor.fetchall()
|
| 26 |
+
if not rows:
|
| 27 |
+
print("No notion configs found.")
|
| 28 |
+
for row in rows:
|
| 29 |
+
key, val = row
|
| 30 |
+
if key == 'notion_integration_token' and val:
|
| 31 |
+
masked = val[:15] + "..." + val[-5:] if len(val) > 20 else val
|
| 32 |
+
print(f"{key}: {masked} (Len: {len(val)})")
|
| 33 |
+
else:
|
| 34 |
+
print(f"{key}: {val}")
|
| 35 |
+
conn.close()
|
| 36 |
+
except Exception as e:
|
| 37 |
+
print(f"Error checking {db_path}: {e}")
|
| 38 |
+
|
force_update_menu.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app import create_app
|
| 2 |
+
from app.database import db, SystemConfig
|
| 3 |
+
import json
|
| 4 |
+
from app.routes import merge_admin_menu_defaults, get_default_admin_menu
|
| 5 |
+
|
| 6 |
+
def update_menu():
|
| 7 |
+
app = create_app()
|
| 8 |
+
with app.app_context():
|
| 9 |
+
raw = SystemConfig.get_config('admin_menu_config_v1')
|
| 10 |
+
if not raw:
|
| 11 |
+
# ์ค์ ์ด ์์ผ๋ฉด ๊ธฐ๋ณธ๊ฐ์ ์ ์ฅ
|
| 12 |
+
default_menu = get_default_admin_menu()
|
| 13 |
+
SystemConfig.set_config('admin_menu_config_v1', json.dumps(default_menu, ensure_ascii=False, indent=2))
|
| 14 |
+
print("Default menu saved to DB")
|
| 15 |
+
return
|
| 16 |
+
|
| 17 |
+
obj = json.loads(raw) if isinstance(raw, str) else raw
|
| 18 |
+
new_obj = merge_admin_menu_defaults(obj)
|
| 19 |
+
|
| 20 |
+
# ์์ด์ ํธ ์น์
์ด ํฌํจ๋์๋์ง ํ์ธ
|
| 21 |
+
has_agent = any(s.get('label') == '์์ด์ ํธ' for s in new_obj.get('sections', []))
|
| 22 |
+
if not has_agent:
|
| 23 |
+
# ๊ฐ์ ๋ก ์ถ๊ฐ
|
| 24 |
+
base = get_default_admin_menu()
|
| 25 |
+
agent_section = next((s for s in base.get('sections', []) if s.get('label') == '์์ด์ ํธ'), None)
|
| 26 |
+
if agent_section:
|
| 27 |
+
new_obj['sections'].append(agent_section)
|
| 28 |
+
print("Agent section forcibly added")
|
| 29 |
+
|
| 30 |
+
SystemConfig.set_config('admin_menu_config_v1', json.dumps(new_obj, ensure_ascii=False, indent=2))
|
| 31 |
+
print("Menu updated and saved to DB")
|
| 32 |
+
|
| 33 |
+
if __name__ == "__main__":
|
| 34 |
+
update_menu()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
|
restart_server.ps1
CHANGED
|
@@ -47,10 +47,10 @@ Start-Sleep -Seconds 5
|
|
| 47 |
# ์๋ฒ ์ํ ํ์ธ
|
| 48 |
$port = netstat -ano | findstr ":5001"
|
| 49 |
if ($port) {
|
| 50 |
-
Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')]
|
| 51 |
Write-Host "ํฌํธ 5001์์ ๋ฆฌ์ค๋ ์ค" -ForegroundColor Cyan
|
| 52 |
$port
|
| 53 |
} else {
|
| 54 |
-
Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')]
|
| 55 |
}
|
| 56 |
|
|
|
|
| 47 |
# ์๋ฒ ์ํ ํ์ธ
|
| 48 |
$port = netstat -ano | findstr ":5001"
|
| 49 |
if ($port) {
|
| 50 |
+
Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] ์๋ฒ ์ฌ๋ถํ
์๋ฃ!" -ForegroundColor Green
|
| 51 |
Write-Host "ํฌํธ 5001์์ ๋ฆฌ์ค๋ ์ค" -ForegroundColor Cyan
|
| 52 |
$port
|
| 53 |
} else {
|
| 54 |
+
Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] ์๋ฒ ์์ ์คํจ ๋๋ ์์ง ์์ ์ค..." -ForegroundColor Yellow
|
| 55 |
}
|
| 56 |
|
start_server_background.ps1
CHANGED
|
@@ -4,85 +4,32 @@ $scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
| 4 |
Set-Location $scriptPath
|
| 5 |
|
| 6 |
# ๋ก๊ทธ ํ์ผ ๊ฒฝ๋ก
|
| 7 |
-
$logFile = Join-Path $scriptPath "
|
| 8 |
|
| 9 |
function Write-Log {
|
| 10 |
param([string]$Message)
|
| 11 |
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
| 12 |
$logMessage = "[$timestamp] $Message"
|
| 13 |
Write-Host $logMessage
|
|
|
|
| 14 |
Add-Content -Path $logFile -Value $logMessage
|
| 15 |
}
|
| 16 |
|
| 17 |
-
function Start-ServerProcess {
|
| 18 |
-
Write-Log "์๋ฒ ํ๋ก์ธ์ค ์์ ์ค..."
|
| 19 |
-
|
| 20 |
-
$processInfo = New-Object System.Diagnostics.ProcessStartInfo
|
| 21 |
-
$processInfo.FileName = "python"
|
| 22 |
-
$processInfo.Arguments = "run.py"
|
| 23 |
-
$processInfo.WorkingDirectory = $scriptPath
|
| 24 |
-
$processInfo.UseShellExecute = $false
|
| 25 |
-
$processInfo.RedirectStandardOutput = $true
|
| 26 |
-
$processInfo.RedirectStandardError = $true
|
| 27 |
-
$processInfo.CreateNoWindow = $true
|
| 28 |
-
|
| 29 |
-
$process = New-Object System.Diagnostics.Process
|
| 30 |
-
$process.StartInfo = $processInfo
|
| 31 |
-
|
| 32 |
-
# ์ถ๋ ฅ ๋ฆฌ๋ค์ด๋ ์
|
| 33 |
-
$process.add_OutputDataReceived({
|
| 34 |
-
param($sender, $e)
|
| 35 |
-
if ($e.Data) {
|
| 36 |
-
Write-Log $e.Data
|
| 37 |
-
}
|
| 38 |
-
})
|
| 39 |
-
|
| 40 |
-
$process.add_ErrorDataReceived({
|
| 41 |
-
param($sender, $e)
|
| 42 |
-
if ($e.Data) {
|
| 43 |
-
Write-Log "ERROR: $($e.Data)"
|
| 44 |
-
}
|
| 45 |
-
})
|
| 46 |
-
|
| 47 |
-
$process.Start() | Out-Null
|
| 48 |
-
$process.BeginOutputReadLine()
|
| 49 |
-
$process.BeginErrorReadLine()
|
| 50 |
-
|
| 51 |
-
return $process
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
# ๋ฉ์ธ ๋ฃจํ
|
| 55 |
Write-Log "=== ์๋ฒ ์๋ ์ฌ์์ ์คํฌ๋ฆฝํธ ์์ ==="
|
| 56 |
|
|
|
|
|
|
|
| 57 |
while ($true) {
|
| 58 |
-
|
| 59 |
try {
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
# ํ๋ก์ธ์ค๊ฐ ์ข
๋ฃ๋ ๋๊น์ง ๋๊ธฐ
|
| 64 |
-
$process.WaitForExit()
|
| 65 |
-
|
| 66 |
-
$exitCode = $process.ExitCode
|
| 67 |
-
Write-Log "์๋ฒ ํ๋ก์ธ์ค ์ข
๋ฃ๋จ (Exit Code: $exitCode)"
|
| 68 |
-
|
| 69 |
-
# ํ๋ก์ธ์ค ์ ๋ฆฌ
|
| 70 |
-
if (!$process.HasExited) {
|
| 71 |
-
$process.Kill()
|
| 72 |
-
}
|
| 73 |
-
$process.Dispose()
|
| 74 |
}
|
| 75 |
catch {
|
| 76 |
Write-Log "์ค๋ฅ ๋ฐ์: $_"
|
| 77 |
-
if ($process -and !$process.HasExited) {
|
| 78 |
-
try {
|
| 79 |
-
$process.Kill()
|
| 80 |
-
} catch {}
|
| 81 |
-
}
|
| 82 |
}
|
| 83 |
|
| 84 |
Write-Log "5์ด ํ ์๋ฒ๋ฅผ ์ฌ์์ํฉ๋๋ค..."
|
| 85 |
Start-Sleep -Seconds 5
|
| 86 |
}
|
| 87 |
-
|
| 88 |
-
|
|
|
|
| 4 |
Set-Location $scriptPath
|
| 5 |
|
| 6 |
# ๋ก๊ทธ ํ์ผ ๊ฒฝ๋ก
|
| 7 |
+
$logFile = Join-Path $scriptPath "logs\server_startup.log"
|
| 8 |
|
| 9 |
function Write-Log {
|
| 10 |
param([string]$Message)
|
| 11 |
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
| 12 |
$logMessage = "[$timestamp] $Message"
|
| 13 |
Write-Host $logMessage
|
| 14 |
+
if (!(Test-Path (Split-Path $logFile))) { New-Item -ItemType Directory -Path (Split-Path $logFile) -Force }
|
| 15 |
Add-Content -Path $logFile -Value $logMessage
|
| 16 |
}
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
Write-Log "=== ์๋ฒ ์๋ ์ฌ์์ ์คํฌ๋ฆฝํธ ์์ ==="
|
| 19 |
|
| 20 |
+
$pythonPath = "$scriptPath\venv\Scripts\python.exe"
|
| 21 |
+
|
| 22 |
while ($true) {
|
| 23 |
+
Write-Log "์๋ฒ ํ๋ก์ธ์ค ์์ ์ค..."
|
| 24 |
try {
|
| 25 |
+
# ๊ฐ๋จํ๊ฒ Start-Process๋ก ์คํํ๊ณ ๋๊ธฐ
|
| 26 |
+
$process = Start-Process -FilePath $pythonPath -ArgumentList "run.py" -WorkingDirectory $scriptPath -PassThru -NoNewWindow -Wait
|
| 27 |
+
Write-Log "์๋ฒ ํ๋ก์ธ์ค ์ข
๋ฃ๋จ (Exit Code: $($process.ExitCode))"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
catch {
|
| 30 |
Write-Log "์ค๋ฅ ๋ฐ์: $_"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
Write-Log "5์ด ํ ์๋ฒ๋ฅผ ์ฌ์์ํฉ๋๋ค..."
|
| 34 |
Start-Sleep -Seconds 5
|
| 35 |
}
|
|
|
|
|
|
templates/admin_settings.html
CHANGED
|
@@ -804,10 +804,11 @@
|
|
| 804 |
|
| 805 |
let html = '';
|
| 806 |
currentNotionDatabases.forEach((db, index) => {
|
|
|
|
| 807 |
html += `
|
| 808 |
<div style="display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; background: #fff; border: 1px solid #dadce0; border-radius: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.03);">
|
| 809 |
<div style="flex: 1; min-width: 0;">
|
| 810 |
-
<div style="font-weight: 600; font-size: 13px; color: #202124; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${db.name}</div>
|
| 811 |
<div style="font-size: 11px; color: #5f6368; font-family: monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${db.id}</div>
|
| 812 |
</div>
|
| 813 |
<button onclick="removeDatabase(${index})" style="background: none; border: none; color: #d93025; cursor: pointer; padding: 4px; font-size: 18px; line-height: 1;" title="์ ๊ฑฐ">ร</button>
|
|
@@ -905,10 +906,11 @@
|
|
| 905 |
|
| 906 |
let html = '<div style="padding: 8px; border-bottom: 1px solid #eee; font-weight: 600; font-size: 12px; color: #1a73e8; background: #f8f9fa; display: flex; justify-content: space-between; align-items: center;"><span>์ถ๊ฐํ DB๋ฅผ ์ ํํ์ธ์:</span><button onclick="document.getElementById(\'notionDatabaseList\').style.display=\'none\'" style="background:none; border:none; color:#5f6368; cursor:pointer;">๋ซ๊ธฐ</button></div>';
|
| 907 |
data.databases.forEach(db => {
|
|
|
|
| 908 |
html += `
|
| 909 |
<div class="db-item" onclick="selectNotionDatabase('${db.id}', '${db.title.replace(/'/g, "\\'")}')"
|
| 910 |
style="padding: 10px 12px; cursor: pointer; border-bottom: 1px solid #f1f3f4; font-size: 13px; transition: background 0.2s;">
|
| 911 |
-
<div style="font-weight: 500;">${db.title}</div>
|
| 912 |
<div style="font-size: 11px; color: #5f6368; font-family: monospace;">${db.id}</div>
|
| 913 |
</div>
|
| 914 |
`;
|
|
|
|
| 804 |
|
| 805 |
let html = '';
|
| 806 |
currentNotionDatabases.forEach((db, index) => {
|
| 807 |
+
const aliasText = db.alias ? ` <span style="color: #1a73e8; font-weight: 500;">[๋ณ์นญ: ${escapeHtml(db.alias)}]</span>` : '';
|
| 808 |
html += `
|
| 809 |
<div style="display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; background: #fff; border: 1px solid #dadce0; border-radius: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.03);">
|
| 810 |
<div style="flex: 1; min-width: 0;">
|
| 811 |
+
<div style="font-weight: 600; font-size: 13px; color: #202124; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${escapeHtml(db.name)}${aliasText}</div>
|
| 812 |
<div style="font-size: 11px; color: #5f6368; font-family: monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${db.id}</div>
|
| 813 |
</div>
|
| 814 |
<button onclick="removeDatabase(${index})" style="background: none; border: none; color: #d93025; cursor: pointer; padding: 4px; font-size: 18px; line-height: 1;" title="์ ๊ฑฐ">ร</button>
|
|
|
|
| 906 |
|
| 907 |
let html = '<div style="padding: 8px; border-bottom: 1px solid #eee; font-weight: 600; font-size: 12px; color: #1a73e8; background: #f8f9fa; display: flex; justify-content: space-between; align-items: center;"><span>์ถ๊ฐํ DB๋ฅผ ์ ํํ์ธ์:</span><button onclick="document.getElementById(\'notionDatabaseList\').style.display=\'none\'" style="background:none; border:none; color:#5f6368; cursor:pointer;">๋ซ๊ธฐ</button></div>';
|
| 908 |
data.databases.forEach(db => {
|
| 909 |
+
const aliasText = db.alias ? ` <span style="color: #1a73e8; font-weight: 500;">[๋ณ์นญ: ${escapeHtml(db.alias)}]</span>` : '';
|
| 910 |
html += `
|
| 911 |
<div class="db-item" onclick="selectNotionDatabase('${db.id}', '${db.title.replace(/'/g, "\\'")}')"
|
| 912 |
style="padding: 10px 12px; cursor: pointer; border-bottom: 1px solid #f1f3f4; font-size: 13px; transition: background 0.2s;">
|
| 913 |
+
<div style="font-weight: 500;">${escapeHtml(db.title)}${aliasText}</div>
|
| 914 |
<div style="font-size: 11px; color: #5f6368; font-family: monospace;">${db.id}</div>
|
| 915 |
</div>
|
| 916 |
`;
|
templates/agent_notion_db.html
CHANGED
|
@@ -193,8 +193,10 @@
|
|
| 193 |
|
| 194 |
if (!response.ok) throw new Error(data.error || '์ค์ ์ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.');
|
| 195 |
|
|
|
|
|
|
|
| 196 |
selector.innerHTML = '';
|
| 197 |
-
if (
|
| 198 |
const option = document.createElement('option');
|
| 199 |
option.value = '';
|
| 200 |
option.textContent = '์ค์ ๋ DB ์์ (์ฐ๋ ์ค์ ํ์)';
|
|
@@ -202,16 +204,15 @@
|
|
| 202 |
return;
|
| 203 |
}
|
| 204 |
|
| 205 |
-
|
| 206 |
const option = document.createElement('option');
|
| 207 |
option.value = db.id;
|
| 208 |
-
|
|
|
|
|
|
|
| 209 |
selector.appendChild(option);
|
| 210 |
});
|
| 211 |
|
| 212 |
-
// ์ฒซ ๋ฒ์งธ DB ์๋ ์กฐํ ์๋ (์ ํ ์ฌํญ)
|
| 213 |
-
// fetchNotionData();
|
| 214 |
-
|
| 215 |
} catch (error) {
|
| 216 |
console.error('DB ๋ชฉ๋ก ๋ก๋ ์ค๋ฅ:', error);
|
| 217 |
selector.innerHTML = '<option value="">๋ชฉ๋ก ๋ก๋ ์คํจ</option>';
|
|
|
|
| 193 |
|
| 194 |
if (!response.ok) throw new Error(data.error || '์ค์ ์ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.');
|
| 195 |
|
| 196 |
+
const databases = data.databases || [];
|
| 197 |
+
|
| 198 |
selector.innerHTML = '';
|
| 199 |
+
if (databases.length === 0) {
|
| 200 |
const option = document.createElement('option');
|
| 201 |
option.value = '';
|
| 202 |
option.textContent = '์ค์ ๋ DB ์์ (์ฐ๋ ์ค์ ํ์)';
|
|
|
|
| 204 |
return;
|
| 205 |
}
|
| 206 |
|
| 207 |
+
databases.forEach(db => {
|
| 208 |
const option = document.createElement('option');
|
| 209 |
option.value = db.id;
|
| 210 |
+
// ๋ณ์นญ์ด ์์ผ๋ฉด ๋ณ์นญ ํ์, ์์ผ๋ฉด ์ด๋ฆ ํ์
|
| 211 |
+
const displayName = db.alias ? `${db.alias} (${db.name})` : db.name;
|
| 212 |
+
option.textContent = displayName;
|
| 213 |
selector.appendChild(option);
|
| 214 |
});
|
| 215 |
|
|
|
|
|
|
|
|
|
|
| 216 |
} catch (error) {
|
| 217 |
console.error('DB ๋ชฉ๋ก ๋ก๋ ์ค๋ฅ:', error);
|
| 218 |
selector.innerHTML = '<option value="">๋ชฉ๋ก ๋ก๋ ์คํจ</option>';
|
templates/webnovels.html
CHANGED
|
@@ -691,6 +691,35 @@
|
|
| 691 |
</div>
|
| 692 |
</div>
|
| 693 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
<!-- GraphRAG ๊ทธ๋ํ ์๊ฐํ ๋ชจ๋ฌ -->
|
| 695 |
<div id="graphRAGVisualizationModal" class="modal">
|
| 696 |
<div class="modal-content" style="max-width: 1600px; width: 95%; height: 90vh;">
|
|
@@ -741,6 +770,57 @@
|
|
| 741 |
</div>
|
| 742 |
</div>
|
| 743 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
<!-- vis-network ๋ผ์ด๋ธ๋ฌ๋ฆฌ -->
|
| 745 |
<script type="text/javascript" src="https://unpkg.com/vis-network@latest/standalone/umd/vis-network.min.js"></script>
|
| 746 |
|
|
@@ -971,7 +1051,9 @@
|
|
| 971 |
<button class="webnovel-item-btn" onclick="viewWebnovelSummary(${file.id}, '${escapeHtml(file.original_filename)}')" style="background: #17a2b8; color: white; margin-right: 8px;">๐ ์์ฝ ๋ณด๊ธฐ</button>
|
| 972 |
<button class="webnovel-item-btn" onclick="viewWebnovelContent(${file.id}, '${escapeHtml(file.original_filename)}')" style="margin-right: 8px;">๐ ๋ด์ฉ ๋ณด๊ธฐ</button>
|
| 973 |
<button class="webnovel-item-btn" onclick="viewGraphRAG(${file.id}, '${escapeHtml(file.original_filename)}')" style="background: #9c27b0; color: white; margin-right: 8px;">๐ ํ์ฐจ๋ณ ์บ๋ฆญํฐ ๊ด๊ณ ๋ถ์</button>
|
| 974 |
-
<button class="webnovel-item-btn" onclick="
|
|
|
|
|
|
|
| 975 |
</div>
|
| 976 |
`;
|
| 977 |
listContainer.appendChild(fileItem);
|
|
@@ -1817,6 +1899,154 @@
|
|
| 1817 |
document.getElementById('graphRAGModal').classList.remove('active');
|
| 1818 |
}
|
| 1819 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1820 |
// GraphRAG ๊ทธ๋ํ ์๊ฐํ ๊ด๋ จ ๋ณ์
|
| 1821 |
let webnovelGraphData = null;
|
| 1822 |
let webnovelGraphNetwork = null;
|
|
@@ -2364,6 +2594,442 @@
|
|
| 2364 |
webnovelAllGraphData = null;
|
| 2365 |
}
|
| 2366 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2367 |
document.getElementById('graphRAGVisualizationModal').addEventListener('click', function(e) {
|
| 2368 |
if (e.target === this) {
|
| 2369 |
closeGraphRAGVisualizationModal();
|
|
|
|
| 691 |
</div>
|
| 692 |
</div>
|
| 693 |
|
| 694 |
+
<!-- ์ฌ๊ฑด๋ณ ์บ๋ฆญํฐ ๊ด๊ณ ๋ถ์ ๋ชจ๋ฌ -->
|
| 695 |
+
<div id="graphRAGByEventModal" class="modal">
|
| 696 |
+
<div class="modal-content" style="max-width: 1600px; width: 95%; height: 90vh; display: flex; flex-direction: column; padding: 0;">
|
| 697 |
+
<div class="modal-header" style="flex-shrink: 0; padding: 24px 24px 16px 24px; margin-bottom: 0;">
|
| 698 |
+
<div class="modal-title" id="graphRAGByEventModalTitle">์ฌ๊ฑด๋ณ ์บ๋ฆญํฐ ๊ด๊ณ ๋ถ์</div>
|
| 699 |
+
<button class="modal-close" onclick="closeGraphRAGByEventModal()">×</button>
|
| 700 |
+
</div>
|
| 701 |
+
<div style="display: flex; flex: 1; overflow: hidden;">
|
| 702 |
+
<!-- ์ข์ธก ์ฌ์ด๋๋ฐ (์ฌ๊ฑด ๋ชฉ๋ก) -->
|
| 703 |
+
<div id="graphRAGByEventSidebar" style="width: 250px; background: var(--bg-secondary); border-right: 1px solid var(--border); overflow-y: auto; flex-shrink: 0; padding: 16px;">
|
| 704 |
+
<div style="font-size: 14px; font-weight: 600; color: var(--text-secondary); margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid var(--border);">
|
| 705 |
+
์ฌ๊ฑด ๋ชฉ๋ก
|
| 706 |
+
</div>
|
| 707 |
+
<div id="graphRAGByEventList" style="display: flex; flex-direction: column; gap: 4px;">
|
| 708 |
+
<div style="text-align: center; padding: 24px; color: var(--text-secondary); font-size: 13px;">
|
| 709 |
+
์ฌ๊ฑด ๋ชฉ๋ก์ ๋ถ๋ฌ์ค๋ ์ค...
|
| 710 |
+
</div>
|
| 711 |
+
</div>
|
| 712 |
+
</div>
|
| 713 |
+
<!-- ์ฐ์ธก ์ฝํ
์ธ ์์ญ -->
|
| 714 |
+
<div id="graphRAGByEventContent" style="flex: 1; overflow-y: auto; padding: 24px;">
|
| 715 |
+
<div style="text-align: center; padding: 24px; color: var(--text-secondary);">
|
| 716 |
+
์ฌ๊ฑด์ ์ ํํ์ฌ ์์ธ ๋ด์ฉ์ ํ์ธํ์ธ์.
|
| 717 |
+
</div>
|
| 718 |
+
</div>
|
| 719 |
+
</div>
|
| 720 |
+
</div>
|
| 721 |
+
</div>
|
| 722 |
+
|
| 723 |
<!-- GraphRAG ๊ทธ๋ํ ์๊ฐํ ๋ชจ๋ฌ -->
|
| 724 |
<div id="graphRAGVisualizationModal" class="modal">
|
| 725 |
<div class="modal-content" style="max-width: 1600px; width: 95%; height: 90vh;">
|
|
|
|
| 770 |
</div>
|
| 771 |
</div>
|
| 772 |
|
| 773 |
+
<!-- ์บ๋ฆญํฐ ๊ด๊ณ๋ ์๊ฐํ (์ฌ๊ฑด์) ๋ชจ๋ฌ -->
|
| 774 |
+
<div id="graphRAGVisualizationByEventModal" class="modal">
|
| 775 |
+
<div class="modal-content" style="max-width: 1600px; width: 95%; height: 90vh;">
|
| 776 |
+
<div class="modal-header">
|
| 777 |
+
<div class="modal-title" id="graphRAGVisualizationByEventModalTitle">์บ๋ฆญํฐ ๊ด๊ณ๋ ์๊ฐํ (์ฌ๊ฑด์)</div>
|
| 778 |
+
<button class="modal-close" onclick="closeGraphRAGVisualizationByEventModal()">×</button>
|
| 779 |
+
</div>
|
| 780 |
+
<div style="padding: 16px; border-bottom: 1px solid var(--border); background: var(--bg-secondary); display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
|
| 781 |
+
<div style="position: relative;">
|
| 782 |
+
<button id="eventFilterToggleByEvent" onclick="toggleWebnovelEventFilterByEvent()" style="padding: 8px 16px; background: var(--bg-primary); border: 1px solid var(--border); border-radius: 6px; font-size: 14px; cursor: pointer; display: flex; align-items: center; gap: 8px; color: var(--text-primary); font-weight: 500;">
|
| 783 |
+
<span>์ฌ๊ฑด ํํฐ</span>
|
| 784 |
+
<span id="eventFilterToggleIconByEvent" style="font-size: 12px; transition: transform 0.2s;">โผ</span>
|
| 785 |
+
</button>
|
| 786 |
+
<div id="eventFilterDropdownByEvent" style="display: none; position: absolute; top: 100%; left: 0; margin-top: 4px; background: var(--bg-primary); border: 1px solid var(--border); border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 1000; min-width: 250px; max-width: 400px; max-height: 400px; overflow-y: auto;">
|
| 787 |
+
<div style="padding: 12px; border-bottom: 1px solid var(--border);">
|
| 788 |
+
<label style="font-size: 13px; cursor: pointer; padding: 6px 8px; border-radius: 4px; display: flex; align-items: center; transition: background 0.2s;" onmouseover="this.style.background='var(--bg-secondary)'" onmouseout="this.style.background='transparent'">
|
| 789 |
+
<input type="checkbox" id="eventFilterAllByEvent" onchange="handleWebnovelEventFilterAllByEvent()" style="margin-right: 8px; cursor: pointer;">
|
| 790 |
+
<span>์ ์ฒด ์ ํ</span>
|
| 791 |
+
</label>
|
| 792 |
+
</div>
|
| 793 |
+
<div id="eventFilterListByEvent" style="padding: 8px;">
|
| 794 |
+
<!-- ์ฌ๊ฑด ์ฒดํฌ๋ฐ์ค๊ฐ ์ฌ๊ธฐ์ ๋์ ์ผ๋ก ์ถ๊ฐ๋จ -->
|
| 795 |
+
</div>
|
| 796 |
+
</div>
|
| 797 |
+
</div>
|
| 798 |
+
<div style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
|
| 799 |
+
<label style="font-size: 13px;">
|
| 800 |
+
<input type="checkbox" id="showCharactersByEvent" checked onchange="updateGraphVisualizationByEvent()" style="margin-right: 4px;">
|
| 801 |
+
์ธ๋ฌผ
|
| 802 |
+
</label>
|
| 803 |
+
<label style="font-size: 13px; margin-left: 8px;">
|
| 804 |
+
<input type="checkbox" id="showLocationsByEvent" checked onchange="updateGraphVisualizationByEvent()" style="margin-right: 4px;">
|
| 805 |
+
์ฅ์
|
| 806 |
+
</label>
|
| 807 |
+
<label style="font-size: 13px; margin-left: 8px;">
|
| 808 |
+
<input type="checkbox" id="showEventsByEvent" checked onchange="updateGraphVisualizationByEvent()" style="margin-right: 4px;">
|
| 809 |
+
์ฌ๊ฑด
|
| 810 |
+
</label>
|
| 811 |
+
</div>
|
| 812 |
+
<button onclick="resetGraphViewByEvent()" style="padding: 6px 16px; background: var(--accent); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; margin-left: auto;">
|
| 813 |
+
๋ทฐ ๋ฆฌ์
|
| 814 |
+
</button>
|
| 815 |
+
</div>
|
| 816 |
+
<div id="graphRAGVisualizationByEventContent" style="height: calc(90vh - 120px); position: relative; background: var(--bg-primary);">
|
| 817 |
+
<div style="text-align: center; padding: 24px; color: var(--text-secondary); position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">
|
| 818 |
+
์ฌ๊ฑด๊ณผ ๋
ธ๋ ํ์
์ ์ ํํ์ฌ ๊ทธ๋ํ๋ฅผ ํ์ธํ์ธ์.
|
| 819 |
+
</div>
|
| 820 |
+
</div>
|
| 821 |
+
</div>
|
| 822 |
+
</div>
|
| 823 |
+
|
| 824 |
<!-- vis-network ๋ผ์ด๋ธ๋ฌ๋ฆฌ -->
|
| 825 |
<script type="text/javascript" src="https://unpkg.com/vis-network@latest/standalone/umd/vis-network.min.js"></script>
|
| 826 |
|
|
|
|
| 1051 |
<button class="webnovel-item-btn" onclick="viewWebnovelSummary(${file.id}, '${escapeHtml(file.original_filename)}')" style="background: #17a2b8; color: white; margin-right: 8px;">๐ ์์ฝ ๋ณด๊ธฐ</button>
|
| 1052 |
<button class="webnovel-item-btn" onclick="viewWebnovelContent(${file.id}, '${escapeHtml(file.original_filename)}')" style="margin-right: 8px;">๐ ๋ด์ฉ ๋ณด๊ธฐ</button>
|
| 1053 |
<button class="webnovel-item-btn" onclick="viewGraphRAG(${file.id}, '${escapeHtml(file.original_filename)}')" style="background: #9c27b0; color: white; margin-right: 8px;">๐ ํ์ฐจ๋ณ ์บ๋ฆญํฐ ๊ด๊ณ ๋ถ์</button>
|
| 1054 |
+
<button class="webnovel-item-btn" onclick="viewGraphRAGByEvent(${file.id}, '${escapeHtml(file.original_filename)}')" style="background: #e91e63; color: white; margin-right: 8px;">๐
์ฌ๊ฑด๋ณ ์บ๋ฆญํฐ ๊ด๊ณ ๋ถ์</button>
|
| 1055 |
+
<button class="webnovel-item-btn" onclick="viewGraphRAGVisualization(${file.id}, '${escapeHtml(file.original_filename)}')" style="background: #28a745; color: white; margin-right: 8px;">๐ ์บ๋ฆญํฐ ๊ด๊ณ๋ ์๊ฐํ</button>
|
| 1056 |
+
<button class="webnovel-item-btn" onclick="viewGraphRAGVisualizationByEvent(${file.id}, '${escapeHtml(file.original_filename)}')" style="background: #ff9800; color: white;">๐ ์บ๋ฆญํฐ ๊ด๊ณ๋ ์๊ฐํ (์ฌ๊ฑด์)</button>
|
| 1057 |
</div>
|
| 1058 |
`;
|
| 1059 |
listContainer.appendChild(fileItem);
|
|
|
|
| 1899 |
document.getElementById('graphRAGModal').classList.remove('active');
|
| 1900 |
}
|
| 1901 |
|
| 1902 |
+
async function viewGraphRAGByEvent(fileId, fileName) {
|
| 1903 |
+
const modal = document.getElementById('graphRAGByEventModal');
|
| 1904 |
+
const title = document.getElementById('graphRAGByEventModalTitle');
|
| 1905 |
+
const content = document.getElementById('graphRAGByEventContent');
|
| 1906 |
+
const sidebar = document.getElementById('graphRAGByEventList');
|
| 1907 |
+
|
| 1908 |
+
title.textContent = `์ฌ๊ฑด๋ณ ์บ๋ฆญํฐ ๊ด๊ณ ๋ถ์ - ${fileName}`;
|
| 1909 |
+
content.innerHTML = '<div style="text-align: center; padding: 24px; color: var(--text-secondary);">์ฌ๊ฑด์ ์ ํํ์ฌ ์์ธ ๋ด์ฉ์ ํ์ธํ์ธ์.</div>';
|
| 1910 |
+
sidebar.innerHTML = '<div style="text-align: center; padding: 24px; color: var(--text-secondary); font-size: 13px;">์ฌ๊ฑด ๋ชฉ๋ก์ ๋ถ๋ฌ์ค๋ ์ค...</div>';
|
| 1911 |
+
modal.classList.add('active');
|
| 1912 |
+
|
| 1913 |
+
try {
|
| 1914 |
+
const response = await fetch(`/api/files/${fileId}/graph`, {
|
| 1915 |
+
credentials: 'include'
|
| 1916 |
+
});
|
| 1917 |
+
if (!response.ok) throw new Error('GraphRAG ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.');
|
| 1918 |
+
|
| 1919 |
+
const data = await response.json();
|
| 1920 |
+
|
| 1921 |
+
// ๋ชจ๋ ์ฌ๊ฑด ์์ง (ํ์๋ผ์ธ ์์๋ก ์ ๋ ฌ)
|
| 1922 |
+
const allEvents = [];
|
| 1923 |
+
const episodes = sortEpisodesByNumber(data.episodes || []);
|
| 1924 |
+
|
| 1925 |
+
episodes.forEach(episode => {
|
| 1926 |
+
const events = data.events_by_episode?.[episode] || [];
|
| 1927 |
+
events.forEach(event => {
|
| 1928 |
+
allEvents.push({
|
| 1929 |
+
...event,
|
| 1930 |
+
episode: episode
|
| 1931 |
+
});
|
| 1932 |
+
});
|
| 1933 |
+
});
|
| 1934 |
+
|
| 1935 |
+
// ์ฌ๊ฑด์ ํ์๋ผ์ธ ์์๋ก ์ ๋ ฌ (ํ์ฐจ ์์ + ์ฌ๊ฑด ์์)
|
| 1936 |
+
allEvents.sort((a, b) => {
|
| 1937 |
+
const episodeA = episodes.indexOf(a.episode);
|
| 1938 |
+
const episodeB = episodes.indexOf(b.episode);
|
| 1939 |
+
if (episodeA !== episodeB) {
|
| 1940 |
+
return episodeA - episodeB;
|
| 1941 |
+
}
|
| 1942 |
+
// ๊ฐ์ ํ์ฐจ ๋ด์์๋ ์ฌ๊ฑด ์ด๋ฆ์ผ๋ก ์ ๋ ฌ
|
| 1943 |
+
return (a.event_name || '').localeCompare(b.event_name || '');
|
| 1944 |
+
});
|
| 1945 |
+
|
| 1946 |
+
// ์ฌ์ด๋๋ฐ์ ์ฌ๊ฑด ๋ชฉ๋ก ํ์
|
| 1947 |
+
sidebar.innerHTML = '';
|
| 1948 |
+
if (allEvents.length === 0) {
|
| 1949 |
+
sidebar.innerHTML = '<div style="text-align: center; padding: 24px; color: var(--text-secondary); font-size: 13px;">์ฌ๊ฑด์ด ์์ต๋๋ค.</div>';
|
| 1950 |
+
} else {
|
| 1951 |
+
allEvents.forEach((event, index) => {
|
| 1952 |
+
const eventId = `event-${index}`;
|
| 1953 |
+
const item = document.createElement('div');
|
| 1954 |
+
item.className = 'episode-sidebar-item';
|
| 1955 |
+
item.style.cssText = 'padding: 10px; cursor: pointer; border-radius: 6px; margin-bottom: 4px; transition: background 0.2s;';
|
| 1956 |
+
item.onmouseover = function() { this.style.background = 'var(--bg-tertiary)'; };
|
| 1957 |
+
item.onmouseout = function() {
|
| 1958 |
+
if (!this.classList.contains('active')) {
|
| 1959 |
+
this.style.background = 'transparent';
|
| 1960 |
+
}
|
| 1961 |
+
};
|
| 1962 |
+
item.textContent = event.event_name || `์ฌ๊ฑด ${index + 1}`;
|
| 1963 |
+
item.onclick = () => showEventDetails(event, eventId, allEvents);
|
| 1964 |
+
if (index === 0) {
|
| 1965 |
+
item.classList.add('active');
|
| 1966 |
+
item.style.background = 'var(--bg-tertiary)';
|
| 1967 |
+
}
|
| 1968 |
+
sidebar.appendChild(item);
|
| 1969 |
+
});
|
| 1970 |
+
|
| 1971 |
+
// ์ฒซ ๋ฒ์งธ ์ฌ๊ฑด ํ์
|
| 1972 |
+
if (allEvents.length > 0) {
|
| 1973 |
+
showEventDetails(allEvents[0], `event-0`, allEvents);
|
| 1974 |
+
}
|
| 1975 |
+
}
|
| 1976 |
+
} catch (error) {
|
| 1977 |
+
console.error('์ฌ๊ฑด๋ณ GraphRAG ๋ฐ์ดํฐ ๋ก๋ ์ค๋ฅ:', error);
|
| 1978 |
+
content.innerHTML = '<div style="text-align: center; padding: 24px; color: #ea4335;">์ฌ๊ฑด๋ณ GraphRAG ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.</div>';
|
| 1979 |
+
}
|
| 1980 |
+
}
|
| 1981 |
+
|
| 1982 |
+
function showEventDetails(event, eventId, allEvents) {
|
| 1983 |
+
const content = document.getElementById('graphRAGByEventContent');
|
| 1984 |
+
const sidebarItems = document.querySelectorAll('#graphRAGByEventList .episode-sidebar-item');
|
| 1985 |
+
|
| 1986 |
+
// ์ฌ์ด๋๋ฐ ํ์ฑํ ์ํ ์
๋ฐ์ดํธ
|
| 1987 |
+
sidebarItems.forEach((item, index) => {
|
| 1988 |
+
if (item.textContent === (event.event_name || `์ฌ๊ฑด ${index + 1}`)) {
|
| 1989 |
+
item.classList.add('active');
|
| 1990 |
+
item.style.background = 'var(--bg-tertiary)';
|
| 1991 |
+
} else {
|
| 1992 |
+
item.classList.remove('active');
|
| 1993 |
+
item.style.background = 'transparent';
|
| 1994 |
+
}
|
| 1995 |
+
});
|
| 1996 |
+
|
| 1997 |
+
let contentHtml = '';
|
| 1998 |
+
contentHtml += `<div style="margin-bottom: 32px; padding: 20px; background: var(--bg-secondary); border-radius: 8px; border-left: 4px solid var(--accent);">`;
|
| 1999 |
+
contentHtml += `<h3 style="font-size: 20px; font-weight: 600; margin-bottom: 12px; color: var(--accent);">${escapeHtml(event.event_name || '์ฌ๊ฑด')}</h3>`;
|
| 2000 |
+
|
| 2001 |
+
if (event.episode) {
|
| 2002 |
+
contentHtml += `<div style="font-size: 14px; color: var(--text-secondary); margin-bottom: 16px;"><strong>ํ์ฐจ:</strong> ${escapeHtml(event.episode)}</div>`;
|
| 2003 |
+
}
|
| 2004 |
+
|
| 2005 |
+
if (event.description) {
|
| 2006 |
+
contentHtml += `<div style="margin-bottom: 20px; padding: 16px; background: var(--bg-primary); border-radius: 6px;">`;
|
| 2007 |
+
contentHtml += `<h4 style="font-size: 16px; font-weight: 600; margin-bottom: 12px; color: #1967d2;">์ค๋ช
</h4>`;
|
| 2008 |
+
contentHtml += `<div style="font-size: 14px; color: var(--text-primary); line-height: 1.6;">${escapeHtml(event.description)}</div>`;
|
| 2009 |
+
contentHtml += `</div>`;
|
| 2010 |
+
}
|
| 2011 |
+
|
| 2012 |
+
if (event.participants && event.participants.length > 0) {
|
| 2013 |
+
contentHtml += `<div style="margin-bottom: 20px;">`;
|
| 2014 |
+
contentHtml += `<h4 style="font-size: 16px; font-weight: 600; margin-bottom: 12px; color: #1967d2;">๊ด๋ จ ์ธ๋ฌผ</h4>`;
|
| 2015 |
+
contentHtml += `<div style="display: flex; flex-wrap: wrap; gap: 8px;">`;
|
| 2016 |
+
event.participants.forEach(participant => {
|
| 2017 |
+
contentHtml += `<span style="padding: 6px 12px; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border); font-size: 13px; color: var(--accent); font-weight: 500;">${escapeHtml(participant)}</span>`;
|
| 2018 |
+
});
|
| 2019 |
+
contentHtml += `</div>`;
|
| 2020 |
+
contentHtml += `</div>`;
|
| 2021 |
+
}
|
| 2022 |
+
|
| 2023 |
+
if (event.location) {
|
| 2024 |
+
contentHtml += `<div style="margin-bottom: 20px;">`;
|
| 2025 |
+
contentHtml += `<h4 style="font-size: 16px; font-weight: 600; margin-bottom: 12px; color: #1967d2;">์ฅ์</h4>`;
|
| 2026 |
+
contentHtml += `<div style="padding: 12px; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border);">`;
|
| 2027 |
+
contentHtml += `<span style="font-size: 14px; color: var(--text-primary); font-weight: 500;">${escapeHtml(event.location)}</span>`;
|
| 2028 |
+
contentHtml += `</div>`;
|
| 2029 |
+
contentHtml += `</div>`;
|
| 2030 |
+
}
|
| 2031 |
+
|
| 2032 |
+
if (event.significance) {
|
| 2033 |
+
contentHtml += `<div style="margin-bottom: 20px;">`;
|
| 2034 |
+
contentHtml += `<h4 style="font-size: 16px; font-weight: 600; margin-bottom: 12px; color: #1967d2;">์ค์๋</h4>`;
|
| 2035 |
+
contentHtml += `<div style="padding: 12px; background: #e8f5e9; border-radius: 6px; border: 1px solid #c8e6c9;">`;
|
| 2036 |
+
contentHtml += `<span style="font-size: 14px; color: #137333; font-weight: 500;">${escapeHtml(event.significance)}</span>`;
|
| 2037 |
+
contentHtml += `</div>`;
|
| 2038 |
+
contentHtml += `</div>`;
|
| 2039 |
+
}
|
| 2040 |
+
|
| 2041 |
+
contentHtml += `</div>`;
|
| 2042 |
+
|
| 2043 |
+
content.innerHTML = contentHtml;
|
| 2044 |
+
}
|
| 2045 |
+
|
| 2046 |
+
function closeGraphRAGByEventModal() {
|
| 2047 |
+
document.getElementById('graphRAGByEventModal').classList.remove('active');
|
| 2048 |
+
}
|
| 2049 |
+
|
| 2050 |
// GraphRAG ๊ทธ๋ํ ์๊ฐํ ๊ด๋ จ ๋ณ์
|
| 2051 |
let webnovelGraphData = null;
|
| 2052 |
let webnovelGraphNetwork = null;
|
|
|
|
| 2594 |
webnovelAllGraphData = null;
|
| 2595 |
}
|
| 2596 |
|
| 2597 |
+
// ์ฌ๊ฑด์ ๊ทธ๋ํ ์๊ฐํ ๊ด๋ จ ๋ณ์
|
| 2598 |
+
let webnovelGraphDataByEvent = null;
|
| 2599 |
+
let webnovelGraphNetworkByEvent = null;
|
| 2600 |
+
let webnovelAllGraphDataByEvent = null;
|
| 2601 |
+
|
| 2602 |
+
async function viewGraphRAGVisualizationByEvent(fileId, fileName) {
|
| 2603 |
+
const modal = document.getElementById('graphRAGVisualizationByEventModal');
|
| 2604 |
+
const title = document.getElementById('graphRAGVisualizationByEventModalTitle');
|
| 2605 |
+
const content = document.getElementById('graphRAGVisualizationByEventContent');
|
| 2606 |
+
|
| 2607 |
+
title.textContent = `์บ๋ฆญํฐ ๊ด๊ณ๋ ์๊ฐํ (์ฌ๊ฑด์) - ${fileName}`;
|
| 2608 |
+
content.innerHTML = '<div style="text-align: center; padding: 24px; color: var(--text-secondary); position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">์ฌ๊ฑด๊ณผ ๋
ธ๋ ํ์
์ ์ ํํ์ฌ ๊ทธ๋ํ๋ฅผ ํ์ธํ์ธ์.</div>';
|
| 2609 |
+
modal.classList.add('active');
|
| 2610 |
+
|
| 2611 |
+
// ๊ธฐ์กด ๋คํธ์ํฌ ์ ๊ฑฐ
|
| 2612 |
+
if (webnovelGraphNetworkByEvent) {
|
| 2613 |
+
webnovelGraphNetworkByEvent.destroy();
|
| 2614 |
+
webnovelGraphNetworkByEvent = null;
|
| 2615 |
+
}
|
| 2616 |
+
|
| 2617 |
+
// ์ฒดํฌ๋ฐ์ค ์ด๊ธฐํ
|
| 2618 |
+
document.getElementById('showCharactersByEvent').checked = true;
|
| 2619 |
+
document.getElementById('showLocationsByEvent').checked = true;
|
| 2620 |
+
document.getElementById('showEventsByEvent').checked = true;
|
| 2621 |
+
|
| 2622 |
+
try {
|
| 2623 |
+
const response = await fetch(`/api/files/${fileId}/graph`, {
|
| 2624 |
+
credentials: 'include'
|
| 2625 |
+
});
|
| 2626 |
+
if (!response.ok) throw new Error('GraphRAG ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.');
|
| 2627 |
+
|
| 2628 |
+
const data = await response.json();
|
| 2629 |
+
webnovelAllGraphDataByEvent = data;
|
| 2630 |
+
|
| 2631 |
+
// ์ฌ๊ฑด ํํฐ ์ฒดํฌ๋ฐ์ค ์์ฑ (ํ์๋ผ์ธ ์์๋ก ์ ๋ ฌ)
|
| 2632 |
+
const eventFilterList = document.getElementById('eventFilterListByEvent');
|
| 2633 |
+
eventFilterList.innerHTML = '';
|
| 2634 |
+
const eventFilterAll = document.getElementById('eventFilterAllByEvent');
|
| 2635 |
+
eventFilterAll.checked = false;
|
| 2636 |
+
|
| 2637 |
+
// ๋ชจ๋ ์ฌ๊ฑด ์์ง
|
| 2638 |
+
const allEvents = [];
|
| 2639 |
+
const episodes = sortEpisodesByNumber(data.episodes || []);
|
| 2640 |
+
|
| 2641 |
+
episodes.forEach(episode => {
|
| 2642 |
+
const events = data.events_by_episode?.[episode] || [];
|
| 2643 |
+
events.forEach(event => {
|
| 2644 |
+
allEvents.push({
|
| 2645 |
+
...event,
|
| 2646 |
+
episode: episode
|
| 2647 |
+
});
|
| 2648 |
+
});
|
| 2649 |
+
});
|
| 2650 |
+
|
| 2651 |
+
// ์ฌ๊ฑด์ ํ์๋ผ์ธ ์์๋ก ์ ๋ ฌ
|
| 2652 |
+
allEvents.sort((a, b) => {
|
| 2653 |
+
const episodeA = episodes.indexOf(a.episode);
|
| 2654 |
+
const episodeB = episodes.indexOf(b.episode);
|
| 2655 |
+
if (episodeA !== episodeB) {
|
| 2656 |
+
return episodeA - episodeB;
|
| 2657 |
+
}
|
| 2658 |
+
return (a.event_name || '').localeCompare(b.event_name || '');
|
| 2659 |
+
});
|
| 2660 |
+
|
| 2661 |
+
if (allEvents.length > 0) {
|
| 2662 |
+
allEvents.forEach((event, index) => {
|
| 2663 |
+
const label = document.createElement('label');
|
| 2664 |
+
label.style.cssText = 'font-size: 13px; cursor: pointer; padding: 6px 12px; border-radius: 4px; display: flex; align-items: center; transition: background 0.2s;';
|
| 2665 |
+
label.onmouseover = function() { this.style.background = 'var(--bg-secondary)'; };
|
| 2666 |
+
label.onmouseout = function() { this.style.background = 'transparent'; };
|
| 2667 |
+
|
| 2668 |
+
const checkbox = document.createElement('input');
|
| 2669 |
+
checkbox.type = 'checkbox';
|
| 2670 |
+
checkbox.value = JSON.stringify({ event_name: event.event_name, episode: event.episode });
|
| 2671 |
+
checkbox.id = `eventFilterByEvent_${index}`;
|
| 2672 |
+
checkbox.onchange = handleWebnovelIndividualEventChangeByEvent;
|
| 2673 |
+
checkbox.style.marginRight = '8px';
|
| 2674 |
+
checkbox.style.cursor = 'pointer';
|
| 2675 |
+
|
| 2676 |
+
const span = document.createElement('span');
|
| 2677 |
+
span.textContent = event.event_name || `์ฌ๊ฑด ${index + 1}`;
|
| 2678 |
+
span.style.flex = '1';
|
| 2679 |
+
|
| 2680 |
+
label.appendChild(checkbox);
|
| 2681 |
+
label.appendChild(span);
|
| 2682 |
+
eventFilterList.appendChild(label);
|
| 2683 |
+
});
|
| 2684 |
+
}
|
| 2685 |
+
|
| 2686 |
+
// ๋ฒํผ ํ
์คํธ ์ด๊ธฐํ
|
| 2687 |
+
updateWebnovelEventFilterButtonTextByEvent();
|
| 2688 |
+
|
| 2689 |
+
// ์ด๊ธฐ์๋ ๊ทธ๋ํ๋ฅผ ์์ฑํ์ง ์์
|
| 2690 |
+
} catch (error) {
|
| 2691 |
+
console.error('GraphRAG ๊ทธ๋ํ ๋ก๋ ์ค๋ฅ:', error);
|
| 2692 |
+
content.innerHTML = '<div style="text-align: center; padding: 24px; color: #ea4335; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">๊ทธ๋ํ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.</div>';
|
| 2693 |
+
}
|
| 2694 |
+
}
|
| 2695 |
+
|
| 2696 |
+
function createWebnovelGraphVisualizationByEvent(data, eventFilter = []) {
|
| 2697 |
+
const content = document.getElementById('graphRAGVisualizationByEventContent');
|
| 2698 |
+
|
| 2699 |
+
// ํํฐ๊ฐ ์ ํ๋์ง ์์๊ฑฐ๋ ๋น ๋ฐฐ์ด์ธ ๊ฒฝ์ฐ ๋น ํ๋ฉด ํ์
|
| 2700 |
+
if (!eventFilter || eventFilter.length === 0) {
|
| 2701 |
+
content.innerHTML = '<div style="text-align: center; padding: 24px; color: var(--text-secondary); position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">์ฌ๊ฑด์ ์ ํํ์ฌ ๊ทธ๋ํ๋ฅผ ํ์ธํ์ธ์.</div>';
|
| 2702 |
+
return;
|
| 2703 |
+
}
|
| 2704 |
+
|
| 2705 |
+
const showCharacters = document.getElementById('showCharactersByEvent').checked;
|
| 2706 |
+
const showLocations = document.getElementById('showLocationsByEvent').checked;
|
| 2707 |
+
const showEvents = document.getElementById('showEventsByEvent').checked;
|
| 2708 |
+
|
| 2709 |
+
// ์ฒดํฌ๋ฐ์ค๊ฐ ๋ชจ๋ ํด์ ๋ ๊ฒฝ์ฐ ๋น ํ๋ฉด ํ์
|
| 2710 |
+
if (!showCharacters && !showLocations && !showEvents) {
|
| 2711 |
+
content.innerHTML = '<div style="text-align: center; padding: 24px; color: var(--text-secondary); position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">๋
ธ๋ ํ์
(์ธ๋ฌผ, ์ฅ์, ์ฌ๊ฑด)์ ํ๋ ์ด์ ์ ํํ์ฌ ๊ทธ๋ํ๋ฅผ ํ์ธํ์ธ์.</div>';
|
| 2712 |
+
return;
|
| 2713 |
+
}
|
| 2714 |
+
|
| 2715 |
+
content.innerHTML = ''; // ๊ธฐ์กด ๋ด์ฉ ์ ๊ฑฐ
|
| 2716 |
+
|
| 2717 |
+
// ๋
ธ๋์ ์ฃ์ง ๋ฐ์ดํฐ ์์ฑ
|
| 2718 |
+
const nodes = new vis.DataSet([]);
|
| 2719 |
+
const edges = new vis.DataSet([]);
|
| 2720 |
+
|
| 2721 |
+
const nodeMap = new Map(); // ๋
ธ๋ ID ๋งคํ
|
| 2722 |
+
const nodeEventsMap = new Map(); // ๋
ธ๋๋ณ ๊ด๋ จ ์ฌ๊ฑด ์ถ์ (nodeId -> Set of event names)
|
| 2723 |
+
let nodeIdCounter = 1;
|
| 2724 |
+
|
| 2725 |
+
// ์ ํ๋ ์ฌ๊ฑด๋ค์ ํ์ฐจ ์์ง
|
| 2726 |
+
const selectedEpisodes = new Set();
|
| 2727 |
+
eventFilter.forEach(eventData => {
|
| 2728 |
+
const event = typeof eventData === 'string' ? JSON.parse(eventData) : eventData;
|
| 2729 |
+
if (event.episode) {
|
| 2730 |
+
selectedEpisodes.add(event.episode);
|
| 2731 |
+
}
|
| 2732 |
+
});
|
| 2733 |
+
|
| 2734 |
+
// ์ํฐํฐ ์ถ๊ฐ (์ธ๋ฌผ, ์ฅ์) - ์ ํ๋ ์ฌ๊ฑด์ด ์๋ ํ์ฐจ๋ง
|
| 2735 |
+
selectedEpisodes.forEach(episode => {
|
| 2736 |
+
const entities = data.entities_by_episode?.[episode] || {};
|
| 2737 |
+
|
| 2738 |
+
// ์ธ๋ฌผ ์ถ๊ฐ
|
| 2739 |
+
if (showCharacters && entities.characters) {
|
| 2740 |
+
entities.characters.forEach(char => {
|
| 2741 |
+
const nodeId = `char_${char.entity_name}`;
|
| 2742 |
+
if (!nodeMap.has(nodeId)) {
|
| 2743 |
+
const id = nodeIdCounter++;
|
| 2744 |
+
nodeMap.set(nodeId, id);
|
| 2745 |
+
nodeEventsMap.set(id, new Set()); // ์ฌ๊ฑด ์ถ์ ์ ์ํ Set ์ด๊ธฐํ
|
| 2746 |
+
nodes.add({
|
| 2747 |
+
id: id,
|
| 2748 |
+
label: char.entity_name,
|
| 2749 |
+
title: `์ธ๋ฌผ: ${char.entity_name}\n์ญํ : ${char.role || '์์'}\n์ค๋ช
: ${char.description || '์์'}`,
|
| 2750 |
+
color: {
|
| 2751 |
+
background: '#4285f4',
|
| 2752 |
+
border: '#1967d2',
|
| 2753 |
+
highlight: { background: '#1a73e8', border: '#1557b0' }
|
| 2754 |
+
},
|
| 2755 |
+
shape: 'ellipse',
|
| 2756 |
+
font: { size: 14, face: 'Inter' },
|
| 2757 |
+
size: 20
|
| 2758 |
+
});
|
| 2759 |
+
}
|
| 2760 |
+
});
|
| 2761 |
+
}
|
| 2762 |
+
|
| 2763 |
+
// ์ฅ์ ์ถ๊ฐ
|
| 2764 |
+
if (showLocations && entities.locations) {
|
| 2765 |
+
entities.locations.forEach(loc => {
|
| 2766 |
+
const nodeId = `loc_${loc.entity_name}`;
|
| 2767 |
+
if (!nodeMap.has(nodeId)) {
|
| 2768 |
+
const id = nodeIdCounter++;
|
| 2769 |
+
nodeMap.set(nodeId, id);
|
| 2770 |
+
nodeEventsMap.set(id, new Set()); // ์ฌ๊ฑด ์ถ์ ์ ์ํ Set ์ด๊ธฐํ
|
| 2771 |
+
nodes.add({
|
| 2772 |
+
id: id,
|
| 2773 |
+
label: loc.entity_name,
|
| 2774 |
+
title: `์ฅ์: ${loc.entity_name}\n์ ํ: ${loc.category || '์์'}\n์ค๋ช
: ${loc.description || '์์'}`,
|
| 2775 |
+
color: {
|
| 2776 |
+
background: '#34a853',
|
| 2777 |
+
border: '#137333',
|
| 2778 |
+
highlight: { background: '#2e7d32', border: '#1b5e20' }
|
| 2779 |
+
},
|
| 2780 |
+
shape: 'box',
|
| 2781 |
+
font: { size: 14, face: 'Inter' },
|
| 2782 |
+
size: 20
|
| 2783 |
+
});
|
| 2784 |
+
}
|
| 2785 |
+
});
|
| 2786 |
+
}
|
| 2787 |
+
});
|
| 2788 |
+
|
| 2789 |
+
// ์ฌ๊ฑด ๋
ธ๋ ์ถ๊ฐ - ์ ํ๋ ์ฌ๊ฑด๋ง
|
| 2790 |
+
if (showEvents) {
|
| 2791 |
+
eventFilter.forEach(eventData => {
|
| 2792 |
+
const event = typeof eventData === 'string' ? JSON.parse(eventData) : eventData;
|
| 2793 |
+
const eventName = event.event_name;
|
| 2794 |
+
const episode = event.episode;
|
| 2795 |
+
|
| 2796 |
+
if (eventName) {
|
| 2797 |
+
const nodeId = `event_${eventName}_${episode}`;
|
| 2798 |
+
if (!nodeMap.has(nodeId)) {
|
| 2799 |
+
const id = nodeIdCounter++;
|
| 2800 |
+
nodeMap.set(nodeId, id);
|
| 2801 |
+
nodeEventsMap.set(id, new Set()); // ์ฌ๊ฑด ์ถ์ ์ ์ํ Set ์ด๊ธฐํ
|
| 2802 |
+
|
| 2803 |
+
// ์ฌ๊ฑด ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
|
| 2804 |
+
const events = data.events_by_episode?.[episode] || [];
|
| 2805 |
+
const eventInfo = events.find(e => e.event_name === eventName) || {};
|
| 2806 |
+
|
| 2807 |
+
nodes.add({
|
| 2808 |
+
id: id,
|
| 2809 |
+
label: eventName,
|
| 2810 |
+
title: `์ฌ๊ฑด: ${eventName}\nํ์ฐจ: ${episode}\n์ค๋ช
: ${eventInfo.description || '์์'}`,
|
| 2811 |
+
color: {
|
| 2812 |
+
background: '#ff9800',
|
| 2813 |
+
border: '#f57c00',
|
| 2814 |
+
highlight: { background: '#fb8c00', border: '#e65100' }
|
| 2815 |
+
},
|
| 2816 |
+
shape: 'diamond',
|
| 2817 |
+
font: { size: 14, face: 'Inter' },
|
| 2818 |
+
size: 20
|
| 2819 |
+
});
|
| 2820 |
+
}
|
| 2821 |
+
}
|
| 2822 |
+
});
|
| 2823 |
+
}
|
| 2824 |
+
|
| 2825 |
+
// ๊ด๊ณ ์ถ๊ฐ - ์ ํ๋ ์ฌ๊ฑด๊ณผ ๊ด๋ จ๋ ๊ด๊ณ๋ง
|
| 2826 |
+
selectedEpisodes.forEach(episode => {
|
| 2827 |
+
const relationships = data.relationships_by_episode?.[episode] || [];
|
| 2828 |
+
relationships.forEach(rel => {
|
| 2829 |
+
// ์ ํ๋ ์ฌ๊ฑด ์ค ํ๋์ ๊ด๋ จ๋ ๊ด๊ณ๋ง ์ถ๊ฐ
|
| 2830 |
+
const relatedEvent = eventFilter.find(eventData => {
|
| 2831 |
+
const event = typeof eventData === 'string' ? JSON.parse(eventData) : eventData;
|
| 2832 |
+
return rel.event === event.event_name;
|
| 2833 |
+
});
|
| 2834 |
+
|
| 2835 |
+
if (relatedEvent) {
|
| 2836 |
+
const event = typeof relatedEvent === 'string' ? JSON.parse(relatedEvent) : relatedEvent;
|
| 2837 |
+
const eventName = event.event_name;
|
| 2838 |
+
|
| 2839 |
+
// source์ target์ด ์ธ๋ฌผ์ธ์ง ์ฅ์์ธ์ง ํ์ธ
|
| 2840 |
+
let sourceNodeId = `char_${rel.source}`;
|
| 2841 |
+
let targetNodeId = `char_${rel.target}`;
|
| 2842 |
+
|
| 2843 |
+
// ์ ํ๋ ํ์ฐจ์ ์ํฐํฐ๋ฅผ ํ์ธํ์ฌ ๋
ธ๋ ID ๊ฒฐ์
|
| 2844 |
+
const entities = data.entities_by_episode?.[episode] || {};
|
| 2845 |
+
const characters = entities.characters || [];
|
| 2846 |
+
const locations = entities.locations || [];
|
| 2847 |
+
|
| 2848 |
+
const sourceIsLocation = locations.some(loc => loc.entity_name === rel.source);
|
| 2849 |
+
const targetIsLocation = locations.some(loc => loc.entity_name === rel.target);
|
| 2850 |
+
|
| 2851 |
+
if (sourceIsLocation) {
|
| 2852 |
+
sourceNodeId = `loc_${rel.source}`;
|
| 2853 |
+
}
|
| 2854 |
+
if (targetIsLocation) {
|
| 2855 |
+
targetNodeId = `loc_${rel.target}`;
|
| 2856 |
+
}
|
| 2857 |
+
|
| 2858 |
+
const sourceId = nodeMap.get(sourceNodeId);
|
| 2859 |
+
const targetId = nodeMap.get(targetNodeId);
|
| 2860 |
+
|
| 2861 |
+
if (sourceId && targetId) {
|
| 2862 |
+
// ๋
ธ๋์ ์ฌ๊ฑด ์ ๋ณด ์ถ๊ฐ
|
| 2863 |
+
if (nodeEventsMap.has(sourceId)) {
|
| 2864 |
+
nodeEventsMap.get(sourceId).add(eventName);
|
| 2865 |
+
}
|
| 2866 |
+
if (nodeEventsMap.has(targetId)) {
|
| 2867 |
+
nodeEventsMap.get(targetId).add(eventName);
|
| 2868 |
+
}
|
| 2869 |
+
|
| 2870 |
+
edges.add({
|
| 2871 |
+
from: sourceId,
|
| 2872 |
+
to: targetId,
|
| 2873 |
+
label: rel.relationship_type || '',
|
| 2874 |
+
title: `${rel.source} โ ${rel.target}\n๊ด๊ณ: ${rel.relationship_type || '์์'}\n์ค๋ช
: ${rel.description || '์์'}`,
|
| 2875 |
+
arrows: 'to',
|
| 2876 |
+
color: { color: '#5f6368', highlight: '#1a73e8' },
|
| 2877 |
+
font: { size: 12, align: 'middle' },
|
| 2878 |
+
smooth: { type: 'continuous' }
|
| 2879 |
+
});
|
| 2880 |
+
|
| 2881 |
+
// ์ฌ๊ฑด ๋
ธ๋์ ์ธ๋ฌผ/์ฅ์ ๋
ธ๋ ์ฐ๊ฒฐ
|
| 2882 |
+
if (showEvents) {
|
| 2883 |
+
const eventNodeId = `event_${eventName}_${episode}`;
|
| 2884 |
+
const eventId = nodeMap.get(eventNodeId);
|
| 2885 |
+
|
| 2886 |
+
if (eventId) {
|
| 2887 |
+
// ์ฌ๊ฑด -> source ์ฐ๊ฒฐ
|
| 2888 |
+
edges.add({
|
| 2889 |
+
from: eventId,
|
| 2890 |
+
to: sourceId,
|
| 2891 |
+
label: '',
|
| 2892 |
+
title: `์ฌ๊ฑด: ${eventName}\nโ ${rel.source}`,
|
| 2893 |
+
arrows: 'to',
|
| 2894 |
+
color: { color: '#ff9800', highlight: '#fb8c00' },
|
| 2895 |
+
font: { size: 11, align: 'middle' },
|
| 2896 |
+
smooth: { type: 'continuous' },
|
| 2897 |
+
dashes: true
|
| 2898 |
+
});
|
| 2899 |
+
|
| 2900 |
+
// ์ฌ๊ฑด -> target ์ฐ๊ฒฐ
|
| 2901 |
+
edges.add({
|
| 2902 |
+
from: eventId,
|
| 2903 |
+
to: targetId,
|
| 2904 |
+
label: '',
|
| 2905 |
+
title: `์ฌ๊ฑด: ${eventName}\nโ ${rel.target}`,
|
| 2906 |
+
arrows: 'to',
|
| 2907 |
+
color: { color: '#ff9800', highlight: '#fb8c00' },
|
| 2908 |
+
font: { size: 11, align: 'middle' },
|
| 2909 |
+
smooth: { type: 'continuous' },
|
| 2910 |
+
dashes: true
|
| 2911 |
+
});
|
| 2912 |
+
}
|
| 2913 |
+
}
|
| 2914 |
+
}
|
| 2915 |
+
}
|
| 2916 |
+
});
|
| 2917 |
+
});
|
| 2918 |
+
|
| 2919 |
+
// ๋
ธ๋์ title์ ์ฌ๊ฑด ์ ๋ณด ์ถ๊ฐ
|
| 2920 |
+
nodes.forEach(node => {
|
| 2921 |
+
const events = nodeEventsMap.get(node.id);
|
| 2922 |
+
if (events && events.size > 0) {
|
| 2923 |
+
const eventsList = Array.from(events).join(', ');
|
| 2924 |
+
const currentTitle = node.title || '';
|
| 2925 |
+
node.title = currentTitle + `\n๊ด๋ จ ์ฌ๊ฑด: ${eventsList}`;
|
| 2926 |
+
nodes.update(node);
|
| 2927 |
+
}
|
| 2928 |
+
});
|
| 2929 |
+
|
| 2930 |
+
if (nodes.length === 0) {
|
| 2931 |
+
content.innerHTML = '<div style="text-align: center; padding: 24px; color: var(--text-secondary); position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">์ ํํ ์ฌ๊ฑด์ ๊ด๋ จ๋ ๋
ธ๋๊ฐ ์์ต๋๋ค.</div>';
|
| 2932 |
+
return;
|
| 2933 |
+
}
|
| 2934 |
+
|
| 2935 |
+
// ๋คํธ์ํฌ ์์ฑ
|
| 2936 |
+
const container = document.createElement('div');
|
| 2937 |
+
container.id = 'graphRAGVisualizationByEventNetwork';
|
| 2938 |
+
container.style.width = '100%';
|
| 2939 |
+
container.style.height = '100%';
|
| 2940 |
+
content.appendChild(container);
|
| 2941 |
+
|
| 2942 |
+
const graphData = { nodes: nodes, edges: edges };
|
| 2943 |
+
const options = {
|
| 2944 |
+
nodes: {
|
| 2945 |
+
borderWidth: 2,
|
| 2946 |
+
shadow: true
|
| 2947 |
+
},
|
| 2948 |
+
edges: {
|
| 2949 |
+
width: 2,
|
| 2950 |
+
shadow: true,
|
| 2951 |
+
smooth: { type: 'continuous' }
|
| 2952 |
+
},
|
| 2953 |
+
physics: {
|
| 2954 |
+
enabled: true,
|
| 2955 |
+
stabilization: { iterations: 200 }
|
| 2956 |
+
},
|
| 2957 |
+
interaction: {
|
| 2958 |
+
hover: true,
|
| 2959 |
+
tooltipDelay: 100,
|
| 2960 |
+
zoomView: true,
|
| 2961 |
+
dragView: true
|
| 2962 |
+
}
|
| 2963 |
+
};
|
| 2964 |
+
|
| 2965 |
+
webnovelGraphNetworkByEvent = new vis.Network(container, graphData, options);
|
| 2966 |
+
webnovelGraphDataByEvent = graphData;
|
| 2967 |
+
}
|
| 2968 |
+
|
| 2969 |
+
function updateGraphVisualizationByEvent() {
|
| 2970 |
+
if (!webnovelAllGraphDataByEvent) return;
|
| 2971 |
+
|
| 2972 |
+
const eventFilter = getSelectedEventsByEvent();
|
| 2973 |
+
createWebnovelGraphVisualizationByEvent(webnovelAllGraphDataByEvent, eventFilter);
|
| 2974 |
+
}
|
| 2975 |
+
|
| 2976 |
+
function getSelectedEventsByEvent() {
|
| 2977 |
+
const checkboxes = document.querySelectorAll('#eventFilterListByEvent input[type="checkbox"]:checked');
|
| 2978 |
+
return Array.from(checkboxes).map(cb => cb.value);
|
| 2979 |
+
}
|
| 2980 |
+
|
| 2981 |
+
function toggleWebnovelEventFilterByEvent() {
|
| 2982 |
+
const dropdown = document.getElementById('eventFilterDropdownByEvent');
|
| 2983 |
+
const icon = document.getElementById('eventFilterToggleIconByEvent');
|
| 2984 |
+
const isVisible = dropdown.style.display !== 'none';
|
| 2985 |
+
dropdown.style.display = isVisible ? 'none' : 'block';
|
| 2986 |
+
icon.style.transform = isVisible ? 'rotate(0deg)' : 'rotate(180deg)';
|
| 2987 |
+
}
|
| 2988 |
+
|
| 2989 |
+
function handleWebnovelEventFilterAllByEvent() {
|
| 2990 |
+
const allCheckbox = document.getElementById('eventFilterAllByEvent');
|
| 2991 |
+
const checkboxes = document.querySelectorAll('#eventFilterListByEvent input[type="checkbox"]');
|
| 2992 |
+
checkboxes.forEach(cb => {
|
| 2993 |
+
cb.checked = allCheckbox.checked;
|
| 2994 |
+
});
|
| 2995 |
+
updateWebnovelEventFilterButtonTextByEvent();
|
| 2996 |
+
updateGraphVisualizationByEvent();
|
| 2997 |
+
}
|
| 2998 |
+
|
| 2999 |
+
function handleWebnovelIndividualEventChangeByEvent() {
|
| 3000 |
+
const allCheckbox = document.getElementById('eventFilterAllByEvent');
|
| 3001 |
+
const checkboxes = document.querySelectorAll('#eventFilterListByEvent input[type="checkbox"]');
|
| 3002 |
+
const checkedCount = Array.from(checkboxes).filter(cb => cb.checked).length;
|
| 3003 |
+
allCheckbox.checked = checkedCount === checkboxes.length;
|
| 3004 |
+
updateWebnovelEventFilterButtonTextByEvent();
|
| 3005 |
+
updateGraphVisualizationByEvent();
|
| 3006 |
+
}
|
| 3007 |
+
|
| 3008 |
+
function updateWebnovelEventFilterButtonTextByEvent() {
|
| 3009 |
+
const button = document.getElementById('eventFilterToggleByEvent');
|
| 3010 |
+
const selectedCount = getSelectedEventsByEvent().length;
|
| 3011 |
+
const buttonText = button.querySelector('span:first-child');
|
| 3012 |
+
if (selectedCount === 0) {
|
| 3013 |
+
buttonText.textContent = '์ฌ๊ฑด ํํฐ';
|
| 3014 |
+
} else {
|
| 3015 |
+
buttonText.textContent = `์ฌ๊ฑด ํํฐ (${selectedCount}๊ฐ ์ ํ๋จ)`;
|
| 3016 |
+
}
|
| 3017 |
+
}
|
| 3018 |
+
|
| 3019 |
+
function resetGraphViewByEvent() {
|
| 3020 |
+
if (webnovelGraphNetworkByEvent) {
|
| 3021 |
+
webnovelGraphNetworkByEvent.fit();
|
| 3022 |
+
}
|
| 3023 |
+
}
|
| 3024 |
+
|
| 3025 |
+
function closeGraphRAGVisualizationByEventModal() {
|
| 3026 |
+
document.getElementById('graphRAGVisualizationByEventModal').classList.remove('active');
|
| 3027 |
+
if (webnovelGraphNetworkByEvent) {
|
| 3028 |
+
webnovelGraphNetworkByEvent.destroy();
|
| 3029 |
+
webnovelGraphNetworkByEvent = null;
|
| 3030 |
+
}
|
| 3031 |
+
}
|
| 3032 |
+
|
| 3033 |
document.getElementById('graphRAGVisualizationModal').addEventListener('click', function(e) {
|
| 3034 |
if (e.target === this) {
|
| 3035 |
closeGraphRAGVisualizationModal();
|
templates/webtoon_personal_management.html
CHANGED
|
@@ -151,6 +151,124 @@
|
|
| 151 |
margin-bottom: 20px;
|
| 152 |
}
|
| 153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
/* Gantt ์คํ์ผ */
|
| 155 |
.gantt-wrapper {
|
| 156 |
display: flex;
|
|
@@ -342,6 +460,70 @@
|
|
| 342 |
๐
์ด๋ฒ ์ฃผ: {{ start_of_week }} ~ {{ end_of_week }}
|
| 343 |
</div>
|
| 344 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
<!-- ๊ณต์ง์ฌํญ ์น์
-->
|
| 346 |
<div id="noticeContainer" class="notice-container">
|
| 347 |
<!-- ๊ณต์ง์ฌํญ์ด ์ฌ๊ธฐ์ ๋ก๋๋ฉ๋๋ค -->
|
|
@@ -389,37 +571,6 @@
|
|
| 389 |
</div>
|
| 390 |
{% endif %}
|
| 391 |
|
| 392 |
-
<!-- Notion ์ผ์ ํ ์น์
(Gantt ์ ์ฉ) -->
|
| 393 |
-
<div class="notion-schedule-card">
|
| 394 |
-
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
| 395 |
-
<h2 style="font-size: 20px; font-weight: 700; margin: 0;">๐
Notion ์ผ์ ํ (์ง๋ ์ฃผ ~ ์ด๋ฒ ์ฃผ)</h2>
|
| 396 |
-
<button onclick="loadNotionSchedule()" style="padding: 6px 12px; font-size: 12px; background: var(--accent); color: white; border: none; border-radius: 4px; cursor: pointer;">์๋ก๊ณ ์นจ</button>
|
| 397 |
-
</div>
|
| 398 |
-
|
| 399 |
-
<div id="notionLoading" class="loading-spinner" style="display: none;">
|
| 400 |
-
<div class="spinner-icon"></div>
|
| 401 |
-
๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...
|
| 402 |
-
</div>
|
| 403 |
-
|
| 404 |
-
<div id="notionGanttSection" style="margin-top: 16px; display: none;">
|
| 405 |
-
<div class="gantt-wrapper" id="notionGanttWrapper" style="height: auto; min-height: 200px;">
|
| 406 |
-
<div class="gantt-side">
|
| 407 |
-
<div class="gantt-side-header">์
๋ฌด๋ช
</div>
|
| 408 |
-
<div class="gantt-side-list" id="notionGanttSideList"></div>
|
| 409 |
-
</div>
|
| 410 |
-
<div class="gantt-right-col">
|
| 411 |
-
<div class="gantt-chart-container" id="notionGanttContainer">
|
| 412 |
-
<svg id="notionGanttSvg"></svg>
|
| 413 |
-
</div>
|
| 414 |
-
</div>
|
| 415 |
-
</div>
|
| 416 |
-
</div>
|
| 417 |
-
|
| 418 |
-
<div id="notionEmptyState" style="text-align: center; padding: 40px; color: var(--muted); border: 1px dashed var(--border); border-radius: 8px; display: none;">
|
| 419 |
-
์ผ์ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.
|
| 420 |
-
</div>
|
| 421 |
-
</div>
|
| 422 |
-
|
| 423 |
{% if matched_data %}
|
| 424 |
{% for project in matched_data %}
|
| 425 |
<div style="margin-top: 40px;">
|
|
@@ -659,13 +810,16 @@
|
|
| 659 |
|
| 660 |
async function loadNotionSchedule() {
|
| 661 |
const loading = document.getElementById('notionLoading');
|
|
|
|
| 662 |
const ganttSection = document.getElementById('notionGanttSection');
|
| 663 |
const emptyState = document.getElementById('notionEmptyState');
|
|
|
|
| 664 |
|
| 665 |
-
loading.style.display = 'flex';
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
|
|
|
| 669 |
|
| 670 |
try {
|
| 671 |
let nicknamesToFetch = [];
|
|
@@ -680,11 +834,13 @@
|
|
| 680 |
|
| 681 |
let allResults = [];
|
| 682 |
let dateProperty = null;
|
|
|
|
|
|
|
| 683 |
|
| 684 |
const total = nicknamesToFetch.length;
|
| 685 |
for (let i = 0; i < total; i++) {
|
| 686 |
const name = nicknamesToFetch[i];
|
| 687 |
-
|
| 688 |
|
| 689 |
let url = `/api/admin/webtoon/agent/schedule?t=${new Date().getTime()}&page_size=50`;
|
| 690 |
if (name) {
|
|
@@ -701,12 +857,28 @@
|
|
| 701 |
if (!dateProperty && data.date_property) {
|
| 702 |
dateProperty = data.date_property;
|
| 703 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 704 |
} else {
|
| 705 |
console.error(`Failed to load schedule for ${name}:`, data.error);
|
|
|
|
|
|
|
| 706 |
}
|
| 707 |
}
|
| 708 |
|
| 709 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 710 |
|
| 711 |
if (allResults.length === 0) {
|
| 712 |
emptyState.style.display = 'block';
|
|
@@ -724,20 +896,112 @@
|
|
| 724 |
}
|
| 725 |
|
| 726 |
renderNotionGantt(uniqueResults, dateProperty);
|
| 727 |
-
ganttSection.style.display = 'block';
|
| 728 |
} catch (error) {
|
| 729 |
console.error('Notion schedule load failed:', error);
|
| 730 |
-
emptyState
|
| 731 |
-
|
|
|
|
|
|
|
| 732 |
} finally {
|
| 733 |
-
loading
|
| 734 |
-
|
|
|
|
|
|
|
| 735 |
}
|
| 736 |
}
|
| 737 |
|
| 738 |
function renderNotionGantt(results, datePropName) {
|
| 739 |
const tasks = [];
|
| 740 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
results.forEach((item, idx) => {
|
| 742 |
const props = item.properties;
|
| 743 |
|
|
@@ -762,64 +1026,114 @@
|
|
| 762 |
let endDate = dateDetail.end || startDate;
|
| 763 |
|
| 764 |
if (startDate) {
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
endDate = endDate.split('T')[0];
|
| 768 |
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 778 |
}
|
| 779 |
});
|
| 780 |
-
|
| 781 |
-
if (tasks.length === 0) {
|
| 782 |
-
document.getElementById('notionEmptyState').style.display = 'block';
|
| 783 |
-
document.getElementById('notionGanttSection').style.display = 'none';
|
| 784 |
-
return;
|
| 785 |
-
}
|
| 786 |
|
| 787 |
-
// ์
|
| 788 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 789 |
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
});
|
| 801 |
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
tasks.forEach(t => {
|
| 809 |
-
const div = document.createElement('div');
|
| 810 |
-
div.className = 'gantt-side-item';
|
| 811 |
-
div.textContent = t.name;
|
| 812 |
-
div.title = t.name;
|
| 813 |
-
sideList.appendChild(div);
|
| 814 |
-
});
|
| 815 |
|
| 816 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 817 |
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 823 |
}
|
| 824 |
|
| 825 |
function formatValue(val) {
|
|
|
|
| 151 |
margin-bottom: 20px;
|
| 152 |
}
|
| 153 |
|
| 154 |
+
/* Notion Weekly Dashboard Styles */
|
| 155 |
+
.notion-weekly-dashboard {
|
| 156 |
+
display: grid;
|
| 157 |
+
grid-template-columns: repeat(7, 1fr);
|
| 158 |
+
gap: 12px;
|
| 159 |
+
margin-top: 24px;
|
| 160 |
+
overflow-x: auto;
|
| 161 |
+
padding-bottom: 16px;
|
| 162 |
+
}
|
| 163 |
+
.notion-day-column {
|
| 164 |
+
background: #f8f9fa;
|
| 165 |
+
border: 1px solid var(--border);
|
| 166 |
+
border-radius: 10px;
|
| 167 |
+
min-width: 180px;
|
| 168 |
+
display: flex;
|
| 169 |
+
flex-direction: column;
|
| 170 |
+
overflow: hidden;
|
| 171 |
+
}
|
| 172 |
+
.notion-day-header {
|
| 173 |
+
background: #ffffff;
|
| 174 |
+
border-bottom: 1px solid var(--border);
|
| 175 |
+
padding: 12px;
|
| 176 |
+
text-align: center;
|
| 177 |
+
}
|
| 178 |
+
.notion-day-name {
|
| 179 |
+
font-size: 14px;
|
| 180 |
+
font-weight: 700;
|
| 181 |
+
color: var(--accent);
|
| 182 |
+
margin-bottom: 4px;
|
| 183 |
+
}
|
| 184 |
+
.notion-day-date {
|
| 185 |
+
font-size: 12px;
|
| 186 |
+
color: var(--muted);
|
| 187 |
+
}
|
| 188 |
+
.notion-day-tasks {
|
| 189 |
+
flex: 1;
|
| 190 |
+
padding: 8px;
|
| 191 |
+
display: flex;
|
| 192 |
+
flex-direction: column;
|
| 193 |
+
gap: 8px;
|
| 194 |
+
min-height: 100px;
|
| 195 |
+
}
|
| 196 |
+
.notion-task-card {
|
| 197 |
+
background: #ffffff;
|
| 198 |
+
border: 1px solid #e0e0e0;
|
| 199 |
+
border-radius: 6px;
|
| 200 |
+
padding: 10px;
|
| 201 |
+
font-size: 12px;
|
| 202 |
+
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
| 203 |
+
transition: transform 0.2s;
|
| 204 |
+
}
|
| 205 |
+
.notion-task-card.planned {
|
| 206 |
+
border-left: 4px solid #fbc02d;
|
| 207 |
+
background: #fffdf7;
|
| 208 |
+
}
|
| 209 |
+
.notion-task-card:hover {
|
| 210 |
+
transform: translateY(-2px);
|
| 211 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 212 |
+
}
|
| 213 |
+
.notion-task-badge {
|
| 214 |
+
display: inline-block;
|
| 215 |
+
padding: 1px 4px;
|
| 216 |
+
border-radius: 3px;
|
| 217 |
+
font-size: 10px;
|
| 218 |
+
font-weight: 700;
|
| 219 |
+
margin-right: 4px;
|
| 220 |
+
vertical-align: middle;
|
| 221 |
+
}
|
| 222 |
+
.badge-notion { background: #e8f0fe; color: #1a73e8; }
|
| 223 |
+
.badge-planned { background: #fff8e1; color: #f57f17; }
|
| 224 |
+
.notion-task-title {
|
| 225 |
+
font-weight: 600;
|
| 226 |
+
margin-bottom: 4px;
|
| 227 |
+
color: #3c4043;
|
| 228 |
+
word-break: break-all;
|
| 229 |
+
}
|
| 230 |
+
.notion-task-meta {
|
| 231 |
+
font-size: 11px;
|
| 232 |
+
color: #70757a;
|
| 233 |
+
display: flex;
|
| 234 |
+
justify-content: space-between;
|
| 235 |
+
align-items: center;
|
| 236 |
+
}
|
| 237 |
+
.notion-day-column.today {
|
| 238 |
+
border-color: var(--accent);
|
| 239 |
+
box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
|
| 240 |
+
}
|
| 241 |
+
.notion-day-column.today .notion-day-header {
|
| 242 |
+
background: #e8f0fe;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
/* ํต๊ณ ์์ฝ ๋ฐ */
|
| 246 |
+
.notion-stats-bar {
|
| 247 |
+
display: grid;
|
| 248 |
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 249 |
+
gap: 16px;
|
| 250 |
+
margin-bottom: 24px;
|
| 251 |
+
}
|
| 252 |
+
.notion-stat-box {
|
| 253 |
+
background: #ffffff;
|
| 254 |
+
border: 1px solid var(--border);
|
| 255 |
+
border-radius: 8px;
|
| 256 |
+
padding: 16px;
|
| 257 |
+
text-align: center;
|
| 258 |
+
}
|
| 259 |
+
.notion-stat-label {
|
| 260 |
+
font-size: 12px;
|
| 261 |
+
color: var(--muted);
|
| 262 |
+
margin-bottom: 4px;
|
| 263 |
+
text-transform: uppercase;
|
| 264 |
+
font-weight: 600;
|
| 265 |
+
}
|
| 266 |
+
.notion-stat-value {
|
| 267 |
+
font-size: 20px;
|
| 268 |
+
font-weight: 700;
|
| 269 |
+
color: var(--text-primary);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
/* Gantt ์คํ์ผ */
|
| 273 |
.gantt-wrapper {
|
| 274 |
display: flex;
|
|
|
|
| 460 |
๐
์ด๋ฒ ์ฃผ: {{ start_of_week }} ~ {{ end_of_week }}
|
| 461 |
</div>
|
| 462 |
|
| 463 |
+
<!-- Notion ์ผ์ ํ ์น์
-->
|
| 464 |
+
<div class="notion-schedule-card">
|
| 465 |
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
|
| 466 |
+
<h2 style="font-size: 20px; font-weight: 700; margin: 0;">๐
์ฃผ๊ฐ ์
๋ฌด ๋ณด๋</h2>
|
| 467 |
+
<div style="display: flex; gap: 8px;">
|
| 468 |
+
<button onclick="loadNotionSchedule()" style="padding: 6px 12px; font-size: 12px; background: var(--accent); color: white; border: none; border-radius: 4px; cursor: pointer;">์๋ก๊ณ ์นจ</button>
|
| 469 |
+
</div>
|
| 470 |
+
</div>
|
| 471 |
+
|
| 472 |
+
<div id="notionLoading" class="loading-spinner" style="display: none;">
|
| 473 |
+
<div class="spinner-icon"></div>
|
| 474 |
+
<span id="notionLoadingText">Notion ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...</span>
|
| 475 |
+
</div>
|
| 476 |
+
|
| 477 |
+
<!-- ์๋ฌ ๋ฐ ์บ์ ์๋ฆผ ๋ฉ์ธ์ง -->
|
| 478 |
+
<div id="notionAlert" style="display: none; margin-bottom: 16px; padding: 12px 16px; border-radius: 8px; font-size: 14px;">
|
| 479 |
+
</div>
|
| 480 |
+
|
| 481 |
+
<!-- ํต๊ณ ์๏ฟฝ๏ฟฝ๏ฟฝ ๋ฐ -->
|
| 482 |
+
<div id="notionStatsBar" class="notion-stats-bar" style="display: none;">
|
| 483 |
+
<div class="notion-stat-box">
|
| 484 |
+
<div class="notion-stat-label">MONTH</div>
|
| 485 |
+
<div id="statMonth" class="notion-stat-value">-</div>
|
| 486 |
+
</div>
|
| 487 |
+
<div class="notion-stat-box">
|
| 488 |
+
<div class="notion-stat-label">TOTAL TASK</div>
|
| 489 |
+
<div id="statTotal" class="notion-stat-value">0</div>
|
| 490 |
+
</div>
|
| 491 |
+
<div class="notion-stat-box">
|
| 492 |
+
<div class="notion-stat-label">THIS WEEK</div>
|
| 493 |
+
<div id="statThisWeek" class="notion-stat-value">0</div>
|
| 494 |
+
</div>
|
| 495 |
+
<div class="notion-stat-box">
|
| 496 |
+
<div class="notion-stat-label">LAST WEEK</div>
|
| 497 |
+
<div id="statLastWeek" class="notion-stat-value">0</div>
|
| 498 |
+
</div>
|
| 499 |
+
</div>
|
| 500 |
+
|
| 501 |
+
<!-- ์ฃผ๊ฐ ๋์๋ณด๋ (์์ผ๋ณ ์ปฌ๋ผ) -->
|
| 502 |
+
<div style="margin-top: 24px;">
|
| 503 |
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
|
| 504 |
+
<span style="background: #e8f0fe; color: #1a73e8; padding: 4px 12px; border-radius: 6px; font-size: 14px; font-weight: 700;">์ด๋ฒ ์ฃผ</span>
|
| 505 |
+
<h3 style="font-size: 16px; font-weight: 700; margin: 0; color: #3c4043;"> ์ฃผ๊ฐ ์
๋ฌด ๋ณด๋</h3>
|
| 506 |
+
</div>
|
| 507 |
+
<div id="notionWeeklyDashboard" class="notion-weekly-dashboard">
|
| 508 |
+
<!-- ์์ผ๋ณ ์ปฌ๋ผ์ด ์ฌ๊ธฐ์ ๋์ ์ผ๋ก ์์ฑ๋ฉ๋๋ค -->
|
| 509 |
+
</div>
|
| 510 |
+
</div>
|
| 511 |
+
|
| 512 |
+
<div style="margin-top: 40px;">
|
| 513 |
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
|
| 514 |
+
<span style="background: #f1f3f4; color: #5f6368; padding: 4px 12px; border-radius: 6px; font-size: 14px; font-weight: 700;">์ง๋ ์ฃผ</span>
|
| 515 |
+
<h3 style="font-size: 16px; font-weight: 700; margin: 0; color: #3c4043;"> ์ฃผ๊ฐ ์
๋ฌด ๋ณด๋</h3>
|
| 516 |
+
</div>
|
| 517 |
+
<div id="notionLastWeeklyDashboard" class="notion-weekly-dashboard">
|
| 518 |
+
<!-- ์์ผ๋ณ ์ปฌ๋ผ์ด ์ฌ๊ธฐ์ ๋์ ์ผ๋ก ์์ฑ๋ฉ๋๋ค -->
|
| 519 |
+
</div>
|
| 520 |
+
</div>
|
| 521 |
+
|
| 522 |
+
<div id="notionEmptyState" style="text-align: center; padding: 40px; color: var(--muted); border: 1px dashed var(--border); border-radius: 8px; display: none;">
|
| 523 |
+
์ผ์ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.
|
| 524 |
+
</div>
|
| 525 |
+
</div>
|
| 526 |
+
|
| 527 |
<!-- ๊ณต์ง์ฌํญ ์น์
-->
|
| 528 |
<div id="noticeContainer" class="notice-container">
|
| 529 |
<!-- ๊ณต์ง์ฌํญ์ด ์ฌ๊ธฐ์ ๋ก๋๋ฉ๋๋ค -->
|
|
|
|
| 571 |
</div>
|
| 572 |
{% endif %}
|
| 573 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 574 |
{% if matched_data %}
|
| 575 |
{% for project in matched_data %}
|
| 576 |
<div style="margin-top: 40px;">
|
|
|
|
| 810 |
|
| 811 |
async function loadNotionSchedule() {
|
| 812 |
const loading = document.getElementById('notionLoading');
|
| 813 |
+
const loadingText = document.getElementById('notionLoadingText');
|
| 814 |
const ganttSection = document.getElementById('notionGanttSection');
|
| 815 |
const emptyState = document.getElementById('notionEmptyState');
|
| 816 |
+
const alertBox = document.getElementById('notionAlert');
|
| 817 |
|
| 818 |
+
if (loading) loading.style.display = 'flex';
|
| 819 |
+
if (loadingText) loadingText.textContent = '๐ Notion ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฐ๊ฒฐํ๊ณ ์์ต๋๋ค...';
|
| 820 |
+
if (ganttSection) ganttSection.style.display = 'none';
|
| 821 |
+
if (emptyState) emptyState.style.display = 'none';
|
| 822 |
+
if (alertBox) alertBox.style.display = 'none';
|
| 823 |
|
| 824 |
try {
|
| 825 |
let nicknamesToFetch = [];
|
|
|
|
| 834 |
|
| 835 |
let allResults = [];
|
| 836 |
let dateProperty = null;
|
| 837 |
+
let hasNotionError = false;
|
| 838 |
+
let errorDetails = "";
|
| 839 |
|
| 840 |
const total = nicknamesToFetch.length;
|
| 841 |
for (let i = 0; i < total; i++) {
|
| 842 |
const name = nicknamesToFetch[i];
|
| 843 |
+
loadingText.innerHTML = `๐ [${i+1}/${total}] <strong>${name || '์ ์ฒด'}</strong> ๋ด๋น์์ ์ผ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ์ค...`;
|
| 844 |
|
| 845 |
let url = `/api/admin/webtoon/agent/schedule?t=${new Date().getTime()}&page_size=50`;
|
| 846 |
if (name) {
|
|
|
|
| 857 |
if (!dateProperty && data.date_property) {
|
| 858 |
dateProperty = data.date_property;
|
| 859 |
}
|
| 860 |
+
|
| 861 |
+
// ์บ์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์จ ๊ฒฝ์ฐ ์๋ฆผ ํ์
|
| 862 |
+
if (data.is_from_cache) {
|
| 863 |
+
hasNotionError = true;
|
| 864 |
+
errorDetails = data.error || "Notion API ์ฐ๊ฒฐ์ ์คํจํ์ฌ ์ ์ฅ๋ ๋ฐ์ดํฐ๋ฅผ ํ์ํฉ๋๋ค.";
|
| 865 |
+
}
|
| 866 |
} else {
|
| 867 |
console.error(`Failed to load schedule for ${name}:`, data.error);
|
| 868 |
+
hasNotionError = true;
|
| 869 |
+
errorDetails = data.error;
|
| 870 |
}
|
| 871 |
}
|
| 872 |
|
| 873 |
+
if (hasNotionError) {
|
| 874 |
+
alertBox.style.display = 'block';
|
| 875 |
+
alertBox.style.backgroundColor = '#fce8e6';
|
| 876 |
+
alertBox.style.color = '#c5221f';
|
| 877 |
+
alertBox.style.border = '1px solid #f5c2c7';
|
| 878 |
+
alertBox.innerHTML = `โ ๏ธ <strong>์ฐ๊ฒฐ ์๋ฆผ:</strong> ${errorDetails}`;
|
| 879 |
+
}
|
| 880 |
+
|
| 881 |
+
loadingText.textContent = '๐ ๋ถ์๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐํ์ผ๋ก ์ฃผ๊ฐ ์
๋ฌด ๋ณด๋๋ฅผ ๊ตฌ์ฑํ๊ณ ์์ต๋๋ค...';
|
| 882 |
|
| 883 |
if (allResults.length === 0) {
|
| 884 |
emptyState.style.display = 'block';
|
|
|
|
| 896 |
}
|
| 897 |
|
| 898 |
renderNotionGantt(uniqueResults, dateProperty);
|
| 899 |
+
if (ganttSection) ganttSection.style.display = 'block';
|
| 900 |
} catch (error) {
|
| 901 |
console.error('Notion schedule load failed:', error);
|
| 902 |
+
if (emptyState) {
|
| 903 |
+
emptyState.textContent = '๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค์ง ๋ชปํ์ต๋๋ค: ' + error.message;
|
| 904 |
+
emptyState.style.display = 'block';
|
| 905 |
+
}
|
| 906 |
} finally {
|
| 907 |
+
if (loading) {
|
| 908 |
+
loading.style.display = 'none';
|
| 909 |
+
if (loadingText) loadingText.innerHTML = `<div class="spinner-icon"></div> ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ์ค...`;
|
| 910 |
+
}
|
| 911 |
}
|
| 912 |
}
|
| 913 |
|
| 914 |
function renderNotionGantt(results, datePropName) {
|
| 915 |
const tasks = [];
|
| 916 |
|
| 917 |
+
// ์ฃผ๊ฐ ์ผ์ ์ ์ํ ์ปจํ
์ด๋ ์ด๊ธฐํ
|
| 918 |
+
const dashboard = document.getElementById('notionWeeklyDashboard');
|
| 919 |
+
const lastDashboard = document.getElementById('notionLastWeeklyDashboard');
|
| 920 |
+
const statsBar = document.getElementById('notionStatsBar');
|
| 921 |
+
|
| 922 |
+
dashboard.innerHTML = '';
|
| 923 |
+
lastDashboard.innerHTML = '';
|
| 924 |
+
dashboard.style.display = 'grid';
|
| 925 |
+
lastDashboard.style.display = 'grid';
|
| 926 |
+
statsBar.style.display = 'grid';
|
| 927 |
+
|
| 928 |
+
// ๋ ์ง ๊ณ์ฐ (์ค๋ ๊ธฐ์ค ์ด๋ฒ ์ฃผ ์์์ผ ~ ๋ค์ ์ฃผ ์ผ์์ผ)
|
| 929 |
+
const now = new Date();
|
| 930 |
+
const todayStr = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0');
|
| 931 |
+
|
| 932 |
+
const day = now.getDay();
|
| 933 |
+
const diffToMonday = (day === 0 ? 6 : day - 1);
|
| 934 |
+
|
| 935 |
+
const thisMonday = new Date(now);
|
| 936 |
+
thisMonday.setDate(now.getDate() - diffToMonday);
|
| 937 |
+
thisMonday.setHours(0,0,0,0);
|
| 938 |
+
|
| 939 |
+
const lastMonday = new Date(thisMonday);
|
| 940 |
+
lastMonday.setDate(thisMonday.getDate() - 7);
|
| 941 |
+
|
| 942 |
+
const thisSunday = new Date(thisMonday);
|
| 943 |
+
thisSunday.setDate(thisMonday.getDate() + 6);
|
| 944 |
+
thisSunday.setHours(23,59,59,999);
|
| 945 |
+
|
| 946 |
+
const nextMonday = new Date(thisMonday);
|
| 947 |
+
nextMonday.setDate(thisMonday.getDate() + 7);
|
| 948 |
+
nextMonday.setHours(0,0,0,0);
|
| 949 |
+
|
| 950 |
+
const nextSunday = new Date(nextMonday);
|
| 951 |
+
nextSunday.setDate(nextMonday.getDate() + 6);
|
| 952 |
+
nextSunday.setHours(23,59,59,999);
|
| 953 |
+
|
| 954 |
+
const toIsoDate = (d) => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
| 955 |
+
const formatDate = (d) => `${d.getMonth() + 1}/${d.getDate()}`;
|
| 956 |
+
|
| 957 |
+
const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
| 958 |
+
document.getElementById('statMonth').textContent = monthNames[now.getMonth()];
|
| 959 |
+
|
| 960 |
+
// ์์ผ ์ปฌ๋ผ ์์ฑ (์ด๋ฒ ์ฃผ & ์ง๋ ์ฃผ)
|
| 961 |
+
const dayNames = ['์', 'ํ', '์', '๋ชฉ', '๊ธ', 'ํ ', '์ผ'];
|
| 962 |
+
const thisDayColumns = {};
|
| 963 |
+
const lastDayColumns = {};
|
| 964 |
+
|
| 965 |
+
for (let i = 0; i < 7; i++) {
|
| 966 |
+
const dayDt = new Date(thisMonday);
|
| 967 |
+
dayDt.setDate(thisMonday.getDate() + i);
|
| 968 |
+
const dayStr = toIsoDate(dayDt);
|
| 969 |
+
|
| 970 |
+
const col = document.createElement('div');
|
| 971 |
+
col.className = `notion-day-column ${dayStr === todayStr ? 'today' : ''}`;
|
| 972 |
+
col.innerHTML = `
|
| 973 |
+
<div class="notion-day-header">
|
| 974 |
+
<div class="notion-day-name">${dayNames[i]}์์ผ</div>
|
| 975 |
+
<div class="notion-day-date">${formatDate(dayDt)}</div>
|
| 976 |
+
</div>
|
| 977 |
+
<div id="this-day-tasks-${dayStr}" class="notion-day-tasks"></div>
|
| 978 |
+
`;
|
| 979 |
+
dashboard.appendChild(col);
|
| 980 |
+
thisDayColumns[dayStr] = col.querySelector('.notion-day-tasks');
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
for (let i = 0; i < 7; i++) {
|
| 984 |
+
const dayDt = new Date(lastMonday);
|
| 985 |
+
dayDt.setDate(lastMonday.getDate() + i);
|
| 986 |
+
const dayStr = toIsoDate(dayDt);
|
| 987 |
+
|
| 988 |
+
const col = document.createElement('div');
|
| 989 |
+
col.className = 'notion-day-column';
|
| 990 |
+
col.innerHTML = `
|
| 991 |
+
<div class="notion-day-header">
|
| 992 |
+
<div class="notion-day-name">${dayNames[i]}์์ผ</div>
|
| 993 |
+
<div class="notion-day-date">${formatDate(dayDt)}</div>
|
| 994 |
+
</div>
|
| 995 |
+
<div id="last-day-tasks-${dayStr}" class="notion-day-tasks"></div>
|
| 996 |
+
`;
|
| 997 |
+
lastDashboard.appendChild(col);
|
| 998 |
+
lastDayColumns[dayStr] = col.querySelector('.notion-day-tasks');
|
| 999 |
+
}
|
| 1000 |
+
|
| 1001 |
+
let totalCount = results.length;
|
| 1002 |
+
let thisWeekCount = 0;
|
| 1003 |
+
let lastWeekCount = 0;
|
| 1004 |
+
|
| 1005 |
results.forEach((item, idx) => {
|
| 1006 |
const props = item.properties;
|
| 1007 |
|
|
|
|
| 1026 |
let endDate = dateDetail.end || startDate;
|
| 1027 |
|
| 1028 |
if (startDate) {
|
| 1029 |
+
const startDateStr = startDate.split('T')[0];
|
| 1030 |
+
const endDateStr = endDate.split('T')[0];
|
|
|
|
| 1031 |
|
| 1032 |
+
const taskCardHtml = `
|
| 1033 |
+
<div class="notion-task-card">
|
| 1034 |
+
<div class="notion-task-title" title="${taskName}">
|
| 1035 |
+
<span class="notion-task-badge badge-notion">NOTION</span>${taskName}
|
| 1036 |
+
</div>
|
| 1037 |
+
<div class="notion-task-meta">
|
| 1038 |
+
<span>๐
${startDateStr === endDateStr ? startDateStr.slice(5) : startDateStr.slice(5) + '-' + endDateStr.slice(5)}</span>
|
| 1039 |
+
</div>
|
| 1040 |
+
</div>
|
| 1041 |
+
`;
|
| 1042 |
+
|
| 1043 |
+
// ์ด๋ฒ ์ฃผ ์์ผ๋ณ ๋์๋ณด๋์ ๋ฐฐ์น
|
| 1044 |
+
for (const [dayStr, container] of Object.entries(thisDayColumns)) {
|
| 1045 |
+
if (startDateStr <= dayStr && endDateStr >= dayStr) {
|
| 1046 |
+
container.insertAdjacentHTML('beforeend', taskCardHtml);
|
| 1047 |
+
}
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
// ์ง๋ ์ฃผ ์์ผ๋ณ ๋์๋ณด๋์ ๋ฐฐ์น
|
| 1051 |
+
for (const [dayStr, container] of Object.entries(lastDayColumns)) {
|
| 1052 |
+
if (startDateStr <= dayStr && endDateStr >= dayStr) {
|
| 1053 |
+
container.insertAdjacentHTML('beforeend', taskCardHtml);
|
| 1054 |
+
}
|
| 1055 |
+
}
|
| 1056 |
+
|
| 1057 |
+
// ํต๊ณ์ฉ ๊ณ์ฐ
|
| 1058 |
+
const thisSunStr = toIsoDate(thisSunday);
|
| 1059 |
+
const thisMonStr = toIsoDate(thisMonday);
|
| 1060 |
+
const lastSunStr = toIsoDate(new Date(thisMonday.getTime() - 86400000));
|
| 1061 |
+
const lastMonStr = toIsoDate(lastMonday);
|
| 1062 |
+
|
| 1063 |
+
if (startDateStr <= thisSunStr && endDateStr >= thisMonStr) {
|
| 1064 |
+
thisWeekCount++;
|
| 1065 |
+
} else if (startDateStr <= lastSunStr && endDateStr >= lastMonStr) {
|
| 1066 |
+
lastWeekCount++;
|
| 1067 |
+
}
|
| 1068 |
}
|
| 1069 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1070 |
|
| 1071 |
+
// (์์ ) ์ด์ฃผ์ ํ ์ผ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋์ ์ถ๊ฐ
|
| 1072 |
+
if (matchedData && matchedData.length > 0) {
|
| 1073 |
+
matchedData.forEach(project => {
|
| 1074 |
+
const fileName = project.file_name;
|
| 1075 |
+
project.episodes.forEach(ep => {
|
| 1076 |
+
const epNum = ep.episode_num;
|
| 1077 |
+
if (ep.stages) {
|
| 1078 |
+
ep.stages.forEach(st => {
|
| 1079 |
+
const stageName = getStageName(st.stage_key, st);
|
| 1080 |
+
const taskName = `[${fileName}] ${epNum}ํ ${stageName}`;
|
| 1081 |
+
const startDateStr = st.start_date;
|
| 1082 |
+
const endDateStr = st.end_date;
|
| 1083 |
|
| 1084 |
+
const taskCardHtml = `
|
| 1085 |
+
<div class="notion-task-card planned">
|
| 1086 |
+
<div class="notion-task-title" title="${taskName}">
|
| 1087 |
+
<span class="notion-task-badge badge-planned">์์ </span>${taskName}
|
| 1088 |
+
</div>
|
| 1089 |
+
<div class="notion-task-meta">
|
| 1090 |
+
<span>๐
${startDateStr === endDateStr ? startDateStr.slice(5) : startDateStr.slice(5) + '-' + endDateStr.slice(5)}</span>
|
| 1091 |
+
</div>
|
| 1092 |
+
</div>
|
| 1093 |
+
`;
|
|
|
|
| 1094 |
|
| 1095 |
+
// ์ด๋ฒ ์ฃผ ์์ผ๋ณ ๋์๋ณด๋์ ๋ฐฐ์น
|
| 1096 |
+
for (const [dayStr, container] of Object.entries(thisDayColumns)) {
|
| 1097 |
+
if (startDateStr <= dayStr && endDateStr >= dayStr) {
|
| 1098 |
+
container.insertAdjacentHTML('beforeend', taskCardHtml);
|
| 1099 |
+
}
|
| 1100 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1101 |
|
| 1102 |
+
// ์ง๋ ์ฃผ ์์ผ๋ณ ๋์๋ณด๋์ ๋ฐฐ์น
|
| 1103 |
+
for (const [dayStr, container] of Object.entries(lastDayColumns)) {
|
| 1104 |
+
if (startDateStr <= dayStr && endDateStr >= dayStr) {
|
| 1105 |
+
container.insertAdjacentHTML('beforeend', taskCardHtml);
|
| 1106 |
+
}
|
| 1107 |
+
}
|
| 1108 |
+
});
|
| 1109 |
+
}
|
| 1110 |
+
});
|
| 1111 |
+
});
|
| 1112 |
+
}
|
| 1113 |
|
| 1114 |
+
document.getElementById('statTotal').textContent = totalCount;
|
| 1115 |
+
document.getElementById('statThisWeek').textContent = thisWeekCount;
|
| 1116 |
+
document.getElementById('statLastWeek').textContent = lastWeekCount;
|
| 1117 |
+
|
| 1118 |
+
// ๋ฐ์ดํฐ ์๋ ์์ผ ์ฒ๋ฆฌ
|
| 1119 |
+
for (const dayStr in thisDayColumns) {
|
| 1120 |
+
if (thisDayColumns[dayStr].innerHTML === '') {
|
| 1121 |
+
thisDayColumns[dayStr].innerHTML = '<div style="text-align: center; padding: 20px; color: #9aa0a6; font-size: 11px; font-style: italic;">์ผ์ ์์</div>';
|
| 1122 |
+
}
|
| 1123 |
+
}
|
| 1124 |
+
for (const dayStr in lastDayColumns) {
|
| 1125 |
+
if (lastDayColumns[dayStr].innerHTML === '') {
|
| 1126 |
+
lastDayColumns[dayStr].innerHTML = '<div style="text-align: center; padding: 20px; color: #9aa0a6; font-size: 11px; font-style: italic;">์ผ์ ์์</div>';
|
| 1127 |
+
}
|
| 1128 |
+
}
|
| 1129 |
+
|
| 1130 |
+
if (results.length === 0) {
|
| 1131 |
+
document.getElementById('notionEmptyState').style.display = 'block';
|
| 1132 |
+
document.getElementById('notionWeeklyDashboard').parentElement.style.display = 'none';
|
| 1133 |
+
document.getElementById('notionLastWeeklyDashboard').parentElement.style.display = 'none';
|
| 1134 |
+
document.getElementById('notionStatsBar').style.display = 'none';
|
| 1135 |
+
return;
|
| 1136 |
+
}
|
| 1137 |
}
|
| 1138 |
|
| 1139 |
function formatValue(val) {
|