From 7b11ba0a8d1e37d51aa95b9022806d95ea74c97e Mon Sep 17 00:00:00 2001 From: seung6lee Date: Wed, 4 Mar 2026 21:41:09 +0900 Subject: [PATCH] class 2-5 --- .gitignore | 175 +++++++++++++++ .prettierrc | 4 + README.md | 15 ++ bun.lockb | Bin 0 -> 43645 bytes client_secret.json | 12 + jsconfig.json | 22 ++ package.json | 19 ++ public/images/calendar.png | Bin 0 -> 305 bytes public/index.html | 47 ++++ public/script.js | 433 +++++++++++++++++++++++++++++++++++++ public/style.css | 304 ++++++++++++++++++++++++++ public/style.css.map | 1 + public/style.scss | 383 ++++++++++++++++++++++++++++++++ server.js | 386 +++++++++++++++++++++++++++++++++ token.json | 8 + 15 files changed, 1809 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 client_secret.json create mode 100644 jsconfig.json create mode 100644 package.json create mode 100644 public/images/calendar.png create mode 100644 public/index.html create mode 100644 public/script.js create mode 100644 public/style.css create mode 100644 public/style.css.map create mode 100644 public/style.scss create mode 100644 server.js create mode 100644 token.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5a938ce --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 4, + "useTabs": false +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..97e0789 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# twofive + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run server.js +``` + +This project was created using `bun init` in bun v1.0.26. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..80f29992ece350877f61c9c1f3a20929af0c7c36 GIT binary patch literal 43645 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 b8*pBd0zpRtdVf6F1;!2cQ;Glo{`>y`&3vkp literal 0 HcmV?d00001 diff --git a/client_secret.json b/client_secret.json new file mode 100644 index 0000000..6041ae4 --- /dev/null +++ b/client_secret.json @@ -0,0 +1,12 @@ +{ + "web": { + "client_id": "769538006975-q773s454k2l1r4q35vat4psbjfgg59pk.apps.googleusercontent.com", + "project_id": "g-cal-task-management25", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_secret": "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", + "redirect_uris": ["http://localhost:4000/callback"], + "javascript_origins": ["http://localhost"] + } +} diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..95c20ce --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + /* Linting */ + "skipLibCheck": true, + "strict": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1834383 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "twofive", + "module": "server.js", + "type": "module", + "devDependencies": { + "@types/bun": "latest", + "sass": "^1.85.1" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "express": "^4.21.2", + "open": "^10.1.0" + }, + "scripts": { + "sass-watch": "sass --watch ./public/style.scss ./public/style.css" + } +} \ No newline at end of file diff --git a/public/images/calendar.png b/public/images/calendar.png new file mode 100644 index 0000000000000000000000000000000000000000..9fe0700a5af6e379a3ab8d17d287db346b5caf37 GIT binary patch literal 305 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|&H|6fVg?3oVGw3ym^DWND9BhG zY9GR_^H(FF7ngQu&X%Q~loCIDuWbbJ5+ literal 0 HcmV?d00001 diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..7aea74e --- /dev/null +++ b/public/index.html @@ -0,0 +1,47 @@ + + + + + + Class 2-5 + + + + + + + +
+
+ + +
+ +
+ + diff --git a/public/script.js b/public/script.js new file mode 100644 index 0000000..c5a7035 --- /dev/null +++ b/public/script.js @@ -0,0 +1,433 @@ +class meal_list extends HTMLElement { + constructor() { + super(); + + const now = new Date(); + const hours = now.getHours(); + + this.date = new Date("2025-03-10") + .toISOString() + .slice(0, 10) + .replace(/-/g, ""); + + switch (true) { + case hours < 8: + this.selectedType = 1; + break; + case hours < 14: + this.selectedType = 2; + break; + default: + this.selectedType = 3; + } + } + + connectedCallback() { + this.render(); + } + + async get_meal(date, type) { + try { + let response = await fetch(`/api/meal?date=${date}&type=${type}`); + + if (!response.ok) throw new Error("Failed to fetch meal data"); + + let data = await response.json(); + + return data; + } catch (error) { + console.error("❌ API Error:", error); + return ["식단 정보를 불러올 수 없습니다."]; + } + } + + async render() { + const header = document.createElement("header"); + + const select = document.createElement("select"); + const types = ["아침", "점심", "저녁"]; + types.forEach((type, index) => { + const option = document.createElement("option"); + option.textContent = types[index]; + option.value = index + 1; + if (index + 1 === this.selectedType) option.selected = true; + select.appendChild(option); + }); + select.addEventListener("change", async () => { + this.selectedType = Number(select.value); + await this.render(); + }); + + const res = await this.get_meal(this.date, this.selectedType); + + const ul = document.createElement("ul"); + res.meal_list.forEach((meal) => { + const li = document.createElement("li"); + li.textContent = meal; + ul.appendChild(li); + }); + + const kcal = document.createElement("p"); + kcal.textContent = res.kcal; + + header.appendChild(select); + header.appendChild(kcal); + + this.innerHTML = ""; + this.append(header); + this.appendChild(ul); + } +} + +customElements.define("meal-list", meal_list); + +class task_list extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + this.render(); + } + + async render() { + const ul = document.createElement("ul"); + + let data = await fetch("/api/tasklist"); + data = await data.json(); + + for (let i of data) { + const date = new Date(i.end.date); + date.setDate(date.getDate() - 1); + + const li = document.createElement("li"); + + const name = document.createElement("h2"); + name.textContent = i.summary; + + const box = document.createElement("div"); + const cal = document.createElement("img"); + cal.src = "/public/images/calendar.png"; + const deadline = document.createElement("p"); + deadline.textContent = date.toISOString().slice(0, 10); + + box.append(cal, deadline); + + li.append(name, box); + + li.addEventListener("click", () => { + const modal = document.createElement("task-edit"); + modal.setAttribute("event_id", i.id); + document.body.appendChild(modal); + }); + + ul.appendChild(li); + } + this.innerHTML = ""; + this.appendChild(ul); + } +} + +customElements.define("task-list", task_list); + +class edit_task extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + this.render(); + } + + async getData() { + const response = await fetch( + `/api/gettask?event_id=${this.getAttribute("event_id")}` + ); + + let data = response.json(); + + return data; + } + + async render() { + this.className = "task_modal"; + + const data = await this.getData(); + + // 📌 닫기 버튼 + const closeBtn = document.createElement("span"); + closeBtn.classList.add("close"); + closeBtn.innerHTML = "✖"; + closeBtn.addEventListener("click", () => this.remove()); + + // 📌 제목 + const title = document.createElement("div"); + title.classList.add("title"); + title.textContent = "Edit Task"; + + // 📌 Name 입력란 + const nameGroup = this.createInputGroup( + "Name", + "text", + "name", + data.summary + ); + + // 📌 Date 입력란 (시작~종료) + const dateGroup = document.createElement("div"); + dateGroup.classList.add("input-group"); + const dateLabel = document.createElement("label"); + dateLabel.textContent = "Date"; + const dateContainer = document.createElement("div"); + dateContainer.classList.add("date-group"); + const startDate = this.createInput( + "date", + "start-date", + data.start.date + ); + const tilde = document.createElement("span"); + tilde.textContent = "~"; + const endDate = this.createInput("date", "end-date", data.end.date); + dateContainer.append(startDate, tilde, endDate); + dateGroup.append(dateLabel, dateContainer); + + // 📌 Description 입력란 + const descGroup = this.createInputGroup( + "Desc", + "textarea", + "desc", + data.description + ); + + // 📌 버튼 컨테이너 + const buttonContainer = document.createElement("div"); + buttonContainer.classList.add("buttons"); + + const cancelBtn = document.createElement("button"); + cancelBtn.classList.add("cancel"); + cancelBtn.textContent = "Cancel"; + cancelBtn.addEventListener("click", () => this.remove()); + + const saveBtn = document.createElement("button"); + saveBtn.classList.add("save"); + saveBtn.textContent = "Save"; + saveBtn.addEventListener("click", async () => await this.saveTask()); + + buttonContainer.append(cancelBtn, saveBtn); + + this.innerHTML = ""; + this.append( + closeBtn, + title, + nameGroup, + dateGroup, + descGroup, + buttonContainer + ); + } + + createInputGroup(labelText, inputType, id, default_value) { + const group = document.createElement("div"); + group.classList.add("input-group"); + + const label = document.createElement("label"); + label.textContent = labelText; + + let input; + if (inputType === "textarea") { + input = document.createElement("textarea"); + input.rows = "4"; + } else { + input = document.createElement("input"); + input.type = inputType; + } + input.value = default_value; + input.placeholder = default_value; + input.id = id; + + group.append(label, input); + return group; + } + + createInput(type, id, default_value) { + const input = document.createElement("input"); + input.type = type; + input.id = id; + input.value = default_value; + input.placeholder = default_value; + return input; + } + + async saveTask() { + const name = document.getElementById("name").value; + const startDate = document.getElementById("start-date").value; + const endDate = document.getElementById("end-date").value; + const desc = document.getElementById("desc").value; + + let response = await fetch("/api/edittask", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + event_id: this.getAttribute("event_id"), + data: { + end: { date: endDate }, + start: { date: startDate }, + summary: name, + description: desc, + }, + }), + }); + response = await response.json(); + console.log(response); + + const taskListComponent = document.querySelector("task-list"); + if (taskListComponent) { + taskListComponent.render(); + } + + const targetIframe = document.getElementById("calendar_iframe"); + if (targetIframe) { + targetIframe.src = targetIframe.src; // ✅ iframe 새로고침 + } + + this.remove(); + } +} + +customElements.define("task-edit", edit_task); + +class create_task extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + this.render(); + } + + async render() { + this.className = "task_modal"; + + // 📌 닫기 버튼 + const closeBtn = document.createElement("span"); + closeBtn.classList.add("close"); + closeBtn.innerHTML = "✖"; + closeBtn.addEventListener("click", () => this.remove()); + + // 📌 제목 + const title = document.createElement("div"); + title.classList.add("title"); + title.textContent = "Create Task"; + + // 📌 Name 입력란 + const nameGroup = this.createInputGroup("Name", "text", "name"); + + // 📌 Date 입력란 (시작~종료) + const dateGroup = document.createElement("div"); + dateGroup.classList.add("input-group"); + const dateLabel = document.createElement("label"); + dateLabel.textContent = "Date"; + const dateContainer = document.createElement("div"); + dateContainer.classList.add("date-group"); + const startDate = this.createInput("date", "start-date"); + const tilde = document.createElement("span"); + tilde.textContent = "~"; + const endDate = this.createInput("date", "end-date"); + dateContainer.append(startDate, tilde, endDate); + dateGroup.append(dateLabel, dateContainer); + + // 📌 Description 입력란 + const descGroup = this.createInputGroup("Desc", "textarea", "desc"); + + // 📌 버튼 컨테이너 + const buttonContainer = document.createElement("div"); + buttonContainer.classList.add("buttons"); + + const cancelBtn = document.createElement("button"); + cancelBtn.classList.add("cancel"); + cancelBtn.textContent = "Cancel"; + cancelBtn.addEventListener("click", () => this.remove()); + + const saveBtn = document.createElement("button"); + saveBtn.classList.add("save"); + saveBtn.textContent = "Save"; + saveBtn.addEventListener("click", async () => await this.saveTask()); + + buttonContainer.append(cancelBtn, saveBtn); + + this.innerHTML = ""; + this.append( + closeBtn, + title, + nameGroup, + dateGroup, + descGroup, + buttonContainer + ); + } + + createInputGroup(labelText, inputType, id) { + const group = document.createElement("div"); + group.classList.add("input-group"); + + const label = document.createElement("label"); + label.textContent = labelText; + + let input; + if (inputType === "textarea") { + input = document.createElement("textarea"); + input.rows = "4"; + } else { + input = document.createElement("input"); + input.type = inputType; + } + input.id = id; + + group.append(label, input); + return group; + } + + createInput(type, id) { + const input = document.createElement("input"); + input.type = type; + input.id = id; + return input; + } + + async saveTask() { + const name = document.getElementById("name").value; + const startDate = document.getElementById("start-date").value; + const endDate = document.getElementById("end-date").value; + const desc = document.getElementById("desc").value; + + let response = await fetch("/api/createtask", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + event_id: this.getAttribute("event_id"), + data: { + end: { date: endDate }, + start: { date: startDate }, + summary: name, + description: desc, + }, + }), + }); + response = await response.json(); + console.log(response); + + const taskListComponent = document.querySelector("task-list"); + if (taskListComponent) { + taskListComponent.render(); + } + + const targetIframe = document.getElementById("calendar_iframe"); + if (targetIframe) { + targetIframe.src = targetIframe.src; // ✅ iframe 새로고침 + } + + this.remove(); + } +} + +customElements.define("create-task", create_task); diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..1794779 --- /dev/null +++ b/public/style.css @@ -0,0 +1,304 @@ +@import url("https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&display=swap"); +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + width: 100%; + height: 100vh; + font-family: "Fira Code", serif; + display: flex; + flex-direction: column; + gap: 10px; + background-color: rgb(246, 242, 237); +} + +.box { + border-radius: 10px; + position: relative; +} + +.title { + height: 30px; + width: 100%; + border-bottom: 2px solid black; + margin-bottom: 10px; + font-size: 20px; + font-weight: 700; + display: flex; + align-items: center; +} + +#header { + width: 100%; + height: 60px; + background-color: white; + box-shadow: 0 1px 1px white; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding-left: 20px; + padding-right: 20px; +} +#header h1 { + font-size: 20px; +} +#header ul { + display: flex; + flex-direction: row; +} +#header ul li { + display: inline-block; + padding: 0 2rem; + font-size: 20px; + font-weight: 700; +} +#header ul li a { + text-decoration: none; + color: black; +} + +#main { + width: 100%; + height: calc(100% - 60px - 10px); + padding: 10px; + display: flex; + flex-direction: row; + gap: 25px; +} + +#side { + width: 400px; + height: 100%; + display: flex; + flex-direction: column; + gap: 25px; +} + +#task { + width: 100%; + height: calc(100% - 515px - 25px); + background-color: white; + border: none; + box-shadow: 0 5px 10px #d4d4d4; + padding: 10px; + display: flex; + flex-direction: column; +} +#task task-list { + width: 100%; + flex-grow: 1; +} +#task task-list ul { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} +#task task-list ul li { + width: 100%; + height: 65px; + border-radius: 10px; + border: 2px solid #b4b4b4; + display: flex; + flex-direction: column; + justify-content: center; + gap: 2px; + padding-left: 10px; + cursor: pointer; +} +#task task-list ul li:hover { + border: 2px solid black; +} +#task task-list ul li h2 { + font-size: 18px; + font-size: 700; +} +#task task-list ul li div { + display: flex; + flex-direction: row; + align-items: center; + gap: 5px; +} +#task task-list ul li div img { + width: 15px; + height: 15px; +} +#task task-list ul li div p { + font-size: 15px; + font-weight: 700; + color: #fbb40e; +} +#task #add_task { + cursor: pointer; + font-size: 25px; + padding-right: 10px; + display: flex; + flex-direction: row-reverse; +} + +#info { + width: 100%; + height: 515px; + background-color: white; + border: none; + box-shadow: 0 5px 10px #d4d4d4; + padding: 10px; +} +#info meal-list { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + gap: 10px; + overflow-y: none; +} +#info meal-list header { + width: 100%; + height: 40px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} +#info meal-list header select { + border: none; + border-radius: 5px; + padding: 4px 4px; + font-size: 15px; + cursor: pointer; +} +#info meal-list header select:hover { + box-shadow: 0 0 0 1px black; +} +#info meal-list ul { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + overflow-y: auto; + overflow-x: hidden; + cursor: ns-resize; +} +#info meal-list ul::-webkit-scrollbar { + display: none; +} +#info meal-list ul li { + width: 100%; + height: 60px; + border-radius: 10px; + border: 2px solid #b4b4b4; + display: flex; + flex-direction: row; + align-items: center; + padding-left: 10px; + flex-shrink: 0; +} + +#calendar { + border: 2px solid orange; + width: calc(100% - 400px - 25px); + height: 100%; +} +#calendar iframe { + border-radius: 8px; + width: 100%; + height: 100%; + border: none; +} + +.task_modal { + width: 400px; + height: auto; + border: 2px solid black; + border-radius: 10px; + background: white; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 10px; + display: flex; + flex-direction: column; + gap: 10px; +} +.task_modal label { + font-weight: 700; + display: block; + margin-bottom: 5px; +} +.task_modal input, +.task_modal textarea { + width: 100%; + padding: 8px; + border: 1px solid black; + border-radius: 4px; +} +.task_modal textarea { + height: 100px; + resize: none; +} +.task_modal .date-group { + display: flex; + align-items: center; + gap: 10px; +} +.task_modal .buttons { + display: flex; + justify-content: flex-end; + gap: 10px; +} +.task_modal button { + padding: 8px 12px; + border-radius: 5px; + cursor: pointer; + font-size: 14px; + font-weight: 700; +} +.task_modal .cancel { + border: 1px solid black; + background: white; +} +.task_modal .save { + background: #d1d1d1; + border: none; +} +.task_modal .close { + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; + font-size: 18px; +} + +@media (max-width: 800px) { + h1 { + font-size: 20px; + } + #main { + width: 100%; + flex-direction: column; + } + #side { + width: 100%; + height: auto; + } + #task { + height: auto; + } + #info { + height: auto; + } + #calendar { + width: 100%; + height: 400px; + flex-shrink: 0; + } +} + +/*# sourceMappingURL=style.css.map */ diff --git a/public/style.css.map b/public/style.css.map new file mode 100644 index 0000000..fc8b9aa --- /dev/null +++ b/public/style.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["style.scss"],"names":[],"mappings":"AAAQ;AAQR;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EAEA;EACA;EACA,KAnBW;EAqBX;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EAOA;EACA;EAEA;EAEA;EACA;EAEA;EACA;;;AAGJ;EACI;EACA,QA/CY;EAgDZ;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;;;AAMhB;EACI;EACA;EAEA,SAxFW;EA0FX;EACA;EAEA,KA5FS;;;AA+Fb;EACI,OA/FQ;EAgGR;EAEA;EACA;EAEA,KAtGS;;;AAyGb;EACI;EACA;EAEA;EACA;EACA;EAEA,SAlHW;EAoHX;EACA;;AAEA;EACI;EACA;;AAEA;EACI;EAEA;EACA;EACA;EACA,KAjIG;;AAmIH;EACI;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA,cA7ID;EA+IC;;AAEA;EACI;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAOpB;EACI;EACA;EAEA;EAEA;EACA;;;AAIR;EACI;EACA,QAzLU;EA2LV;EACA;EACA;EAEA,SAlMW;;AAoMX;EACI;EACA;EAEA;EACA;EACA,KA1MO;EA4MP;;AAEA;EACI;EACA;EAEA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EAEA;EACA;;AAEA;EACI;;AAKZ;EACI;EAEA;EACA;EACA;EACA,KA3OG;EA6OH;EACA;EAEA;;AAEA;EACI;;AAGJ;EACI;EACA;EACA;EACA;EAEA;EACA;EACA;EACA,cA/PD;EAiQC;;;AAMhB;EACI;EAEA;EACA;;AAEA;EACI;EACA;EACA;EACA;;;AAIR;EACI;EACA;EAEA;EACA;EACA;EAEA;EACA;EACA;EACA;EAEA,SAlSW;EAoSX;EACA;EACA,KAtSW;;AAwSX;EACI;EACA;EACA;;AAGJ;AAAA;EAEI;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;IACI;;EAGJ;IACI;IACA;;EAGJ;IACI;IACA;;EAEJ;IACI;;EAEJ;IACI;;EAGJ;IACI;IACA;IACA","file":"style.css"} \ No newline at end of file diff --git a/public/style.scss b/public/style.scss new file mode 100644 index 0000000..dbeafde --- /dev/null +++ b/public/style.scss @@ -0,0 +1,383 @@ +@import url("https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&display=swap"); + +$small_margin: 10px; +$big_margin: 25px; +$nav_width: 400px; +$info_height: calc(60px * 7 + $small_margin * 7 + $big_margin); +$header_height: 60px; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + width: 100%; + height: 100vh; + font-family: "Fira Code", serif; + + display: flex; + flex-direction: column; + gap: $small_margin; + + background-color: rgb(246, 242, 237); +} + +.box { + border-radius: 10px; + position: relative; +} + +.title { + height: 30px; + // background-color: white; + + // position: absolute; + // top: -2px; + // left: 15px; + + width: 100%; + border-bottom: 2px solid black; + + margin-bottom: 10px; + + font-size: 20px; + font-weight: 700; + + display: flex; + align-items: center; +} + +#header { + width: 100%; + height: $header_height; + background-color: white; + box-shadow: 0 1px 1px white; + + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding-left: 20px; + padding-right: 20px; + + h1 { + font-size: 20px; + } + + ul { + display: flex; + flex-direction: row; + + li { + display: inline-block; + padding: 0 2rem; + font-size: 20px; + font-weight: 700; + + a { + text-decoration: none; + color: black; + } + } + } +} + +#main { + width: 100%; + height: calc(100% - $header_height - $small_margin); + + padding: $small_margin; + + display: flex; + flex-direction: row; + + gap: $big_margin; +} + +#side { + width: $nav_width; + height: 100%; + + display: flex; + flex-direction: column; + + gap: $big_margin; +} + +#task { + width: 100%; + height: calc(100% - $info_height - $big_margin); + + background-color: white; + border: none; + box-shadow: 0 5px 10px #d4d4d4; + + padding: $small_margin; + + display: flex; + flex-direction: column; + + task-list { + width: 100%; + flex-grow: 1; + + ul { + width: 100%; + + display: flex; + flex-direction: column; + align-items: center; + gap: $small_margin; + + li { + width: 100%; + height: 65px; + border-radius: 10px; + border: 2px solid #b4b4b4; + + display: flex; + flex-direction: column; + justify-content: center; + gap: 2px; + padding-left: $small_margin; + + cursor: pointer; + + &:hover { + border: 2px solid black; + } + + h2 { + font-size: 18px; + font-size: 700; + } + + div { + display: flex; + flex-direction: row; + align-items: center; + gap: 5px; + + img { + width: 15px; + height: 15px; + } + + p { + font-size: 15px; + font-weight: 700; + color: #fbb40e; + } + } + } + } + } + + #add_task { + cursor: pointer; + font-size: 25px; + + padding-right: 10px; + + display: flex; + flex-direction: row-reverse; + } +} + +#info { + width: 100%; + height: $info_height; + + background-color: white; + border: none; + box-shadow: 0 5px 10px #d4d4d4; + + padding: $small_margin; + + meal-list { + width: 100%; + height: 100%; + + display: flex; + flex-direction: column; + gap: $small_margin; + + overflow-y: none; + + header { + width: 100%; + height: 40px; + + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + select { + border: none; + border-radius: 5px; + padding: 4px 4px; + + font-size: 15px; + cursor: pointer; + + &:hover { + box-shadow: 0 0 0 1px black; + } + } + } + + ul { + width: 100%; + + display: flex; + flex-direction: column; + align-items: center; + gap: $small_margin; + + overflow-y: auto; + overflow-x: hidden; + + cursor: ns-resize; + + &::-webkit-scrollbar { + display: none; + } + + li { + width: 100%; + height: 60px; + border-radius: 10px; + border: 2px solid #b4b4b4; + + display: flex; + flex-direction: row; + align-items: center; + padding-left: $small_margin; + + flex-shrink: 0; + } + } + } +} + +#calendar { + border: 2px solid orange; + + width: calc(100% - $nav_width - $big_margin); + height: 100%; + + iframe { + border-radius: 8px; + width: 100%; + height: 100%; + border: none; + } +} + +.task_modal { + width: 400px; + height: auto; + + border: 2px solid black; + border-radius: 10px; + background: white; + + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + padding: $small_margin; + + display: flex; + flex-direction: column; + gap: $small_margin; + + label { + font-weight: 700; + display: block; + margin-bottom: 5px; + } + + input, + textarea { + width: 100%; + padding: 8px; + border: 1px solid black; + border-radius: 4px; + } + + textarea { + height: 100px; + resize: none; + } + + .date-group { + display: flex; + align-items: center; + gap: 10px; + } + + .buttons { + display: flex; + justify-content: flex-end; + gap: 10px; + } + + button { + padding: 8px 12px; + border-radius: 5px; + cursor: pointer; + font-size: 14px; + font-weight: 700; + } + + .cancel { + border: 1px solid black; + background: white; + } + + .save { + background: #d1d1d1; + border: none; + } + + .close { + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; + font-size: 18px; + } +} + +@media (max-width: 800px) { + h1 { + font-size: 20px; + } + + #main { + width: 100%; + flex-direction: column; + } + + #side { + width: 100%; + height: auto; + } + #task { + height: auto; + } + #info { + height: auto; + } + + #calendar { + width: 100%; + height: 400px; + flex-shrink: 0; + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..a1fed43 --- /dev/null +++ b/server.js @@ -0,0 +1,386 @@ +import express from "express"; +import path from "path"; +import open from "open"; +import fs from "fs"; + +const app = express(); + +app.use("/public", express.static(path.join(__dirname, "public"))); +app.use(express.urlencoded({ extends: true })); +app.use(express.json()); + +app.listen(4000, () => { + console.log("✅Pass: Server running at http://127.0.0.1:4000"); +}); + +app.get("/", (req, res) => { + res.sendFile(__dirname + "/public/index.html"); +}); + +app.get("/api/meal", async (req, res) => { + const item = req.query; + const date = item.date; + const type = item.type; + + const base_url = new URL("https://open.neis.go.kr/hub/mealServiceDietInfo"); + const params = new URLSearchParams({ + key: "504b37988adb4251b9fae916ae8c360a", + Type: "json", + ATPT_OFCDC_SC_CODE: "D10", + SD_SCHUL_CODE: "7240450", + MLSV_YMD: date, + MMEAL_SC_CODE: type, + }); + const url = `${base_url}?${params.toString()}`; + + try { + let data = await fetch(url); + + if (!data.ok) { + res.send("No Information"); + } + + data = await data.json(); + + if (!data.mealServiceDietInfo[1].row[0]) { + res.send("No Information"); + } + + let meal_list = data.mealServiceDietInfo[1].row[0].DDISH_NM; + meal_list = meal_list.replace(/\([^)]+\)/g, "").split("
"); + + res.json({ + meal_list: meal_list, + kcal: data.mealServiceDietInfo[1].row[0].CAL_INFO, + }); + } catch (error) { + res.send("No Information"); + } +}); + +// G-Cal api ------------------------------------------------------------ + +const checkClientSecret = () => { + if (!fs.existsSync("client_secret.json")) { + throw new Error("No client_secret.json"); + } + console.log("✅Pass: client_secret.json exists"); +}; + +const checkToken = () => { + if (!fs.existsSync("token.json")) { + console.error("❌Error: No token.json"); + console.warn("📣Notice: Trying to get token"); + getToken(); + } else { + console.log("✅Pass: token.json exists"); + } +}; + +const getToken = () => { + const base_url = new URL("https://accounts.google.com/o/oauth2/auth"); + const params = new URLSearchParams({ + client_id: client().client_id, + redirect_uri: "http://localhost:4000/callback", + response_type: "code", + scope: "https://www.googleapis.com/auth/calendar", + access_type: "offline", + prompt: "consent", + }); + const url = `${base_url}?${params.toString()}`; + open(url); +}; + +const client = () => { + const fileContent = fs.readFileSync("client_secret.json", "utf8"); + return JSON.parse(fileContent).web; +}; + +const token = (type) => { + const fileContent = fs.readFileSync("token.json", "utf8"); + if (type) { + return JSON.parse(fileContent)[type]; + } + return JSON.parse(fileContent).access_token; +}; + +app.get("/callback", async (req, res) => { + try { + const response = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: {}, + body: JSON.stringify({ + client_id: client().client_id, + client_secret: client().client_secret, + code: req.query.code, + grant_type: "authorization_code", + redirect_uri: "http://localhost:4000/callback", + }), + }); + + const data = await response.json(); + + if (data.error) { + console.error(`❌Error: ${data.error}, ${data.error_description}`); + res.send( + "Error occured. Automatically reopen new Oauth page if token is not saved." + ); + checkToken(); + return; + } + + console.log("✅Pass: Successfully load token"); + saveToken(data); + res.json(data); + } catch (err) { + console.error(`❌Error: ${err.message}`); + res.status(500).json({ error: err.message }); + } +}); + +const saveToken = (data) => { + fs.writeFileSync("token.json", JSON.stringify(data, null, 2)); + console.log("✅Pass: Successfully sace token.json"); +}; + +const refreshToken = async () => { + try { + const response = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: {}, + body: JSON.stringify({ + client_id: client().client_id, + client_secret: client().client_secret, + grant_type: "refresh_token", + refresh_token: token("refresh_token"), + }), + }); + + const data = await response.json(); + + if (data.error) { + return data; + } + + data.refresh_token = token("refresh_token"); + saveToken(data); + return "success"; + } catch (err) { + return err; + } +}; + +checkClientSecret(); +checkToken(); + +app.get("/api/tasklist", async (req, res) => { + const process = async () => { + const task_calender_id = + "729f5bea9ba83ff9b82dd797f15089a051fe850a4da66fcc3936835e89e3de89@group.calendar.google.com"; + const base_url = new URL( + `https://www.googleapis.com/calendar/v3/calendars/${task_calender_id}/events` + ); + const params = new URLSearchParams({ + singleEvents: true, + orderBy: "startTime", + timeMin: new Date().toISOString(), + }); + const url = `${base_url}?${params.toString()}`; + + try { + let data = await fetch(url, { + method: "GET", + headers: { + Authorization: `Bearer ${token()}`, + }, + }); + + data = await data.json(); + + if (data.error) { + if (data.error.code === 401) { + console.warn("📣Notice: Token expired, refreshing..."); + + const refreshStatus = await refreshToken(); + if (refreshStatus === "success") { + console.warn("📣Notice: Token refresh success"); + return await process(); + } else { + console.error("❌Error: Can't refresh token"); + return refreshStatus; + } + } + + console.error( + `❌Error: ${data.error.code}, ${data.error.message}` + ); + + return data; + } + + const li = data.items; + + res.json(li); + } catch (err) { + console.error(`❌Error: ${err}`); + return err; + } + }; + + return await process(); +}); + +app.get("/api/gettask", async (req, res) => { + const process = async () => { + const task_calender_id = + "729f5bea9ba83ff9b82dd797f15089a051fe850a4da66fcc3936835e89e3de89@group.calendar.google.com"; + const base_url = new URL( + `https://www.googleapis.com/calendar/v3/calendars/${task_calender_id}/events/${req.query.event_id}` + ); + const params = new URLSearchParams({ + singleEvents: true, + orderBy: "startTime", + timeMin: new Date().toISOString(), + }); + const url = `${base_url}?${params.toString()}`; + + try { + let data = await fetch(url, { + method: "GET", + headers: { + Authorization: `Bearer ${token()}`, + }, + }); + + data = await data.json(); + + if (data.error) { + if (data.error.code === 401) { + console.warn("📣Notice: Token expired, refreshing..."); + + const refreshStatus = await refreshToken(); + if (refreshStatus === "success") { + console.warn("📣Notice: Token refresh success"); + return await process(); + } else { + console.error("❌Error: Can't refresh token"); + return refreshStatus; + } + } + + console.error( + `❌Error: ${data.error.code}, ${data.error.message}` + ); + + return data; + } + + res.json(data); + } catch (err) { + console.error(`❌Error: ${err}`); + return err; + } + }; + + return await process(); +}); + +app.post("/api/edittask", async (req, res) => { + const process = async () => { + const task_calender_id = + "729f5bea9ba83ff9b82dd797f15089a051fe850a4da66fcc3936835e89e3de89@group.calendar.google.com"; + const url = new URL( + `https://www.googleapis.com/calendar/v3/calendars/${task_calender_id}/events/${req.body.event_id}` + ); + + try { + let data = await fetch(url, { + method: "PUT", + headers: { + Authorization: `Bearer ${token()}`, + }, + body: JSON.stringify(req.body.data), + }); + + data = await data.json(); + + if (data.error) { + if (data.error.code === 401) { + console.warn("📣Notice: Token expired, refreshing..."); + + const refreshStatus = await refreshToken(); + if (refreshStatus === "success") { + console.warn("📣Notice: Token refresh success"); + return await process(); + } else { + console.error("❌Error: Can't refresh token"); + return refreshStatus; + } + } + + console.error( + `❌Error: ${data.error.code}, ${data.error.message}` + ); + + return data; + } + + res.json(data); + } catch (err) { + console.error(`❌Error: ${err}`); + return err; + } + }; + + return await process(); +}); + +app.post("/api/createtask", async (req, res) => { + const process = async () => { + const task_calender_id = + "729f5bea9ba83ff9b82dd797f15089a051fe850a4da66fcc3936835e89e3de89@group.calendar.google.com"; + const url = new URL( + `https://www.googleapis.com/calendar/v3/calendars/${task_calender_id}/events` + ); + + try { + let data = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${token()}`, + }, + body: JSON.stringify(req.body.data), + }); + + data = await data.json(); + + if (data.error) { + if (data.error.code === 401) { + console.warn("📣Notice: Token expired, refreshing..."); + + const refreshStatus = await refreshToken(); + if (refreshStatus === "success") { + console.warn("📣Notice: Token refresh success"); + return await process(); + } else { + console.error("❌Error: Can't refresh token"); + return refreshStatus; + } + } + + console.error( + `❌Error: ${data.error.code}, ${data.error.message}` + ); + + return data; + } + + res.json(data); + } catch (err) { + console.error(`❌Error: ${err}`); + return err; + } + }; + + return await process(); +}); \ No newline at end of file diff --git a/token.json b/token.json new file mode 100644 index 0000000..1290e02 --- /dev/null +++ b/token.json @@ -0,0 +1,8 @@ +{ + "access_token": "ya29.a0AeXRPp6dQRljlR_5KWE8DgcWu4lkKXDAgErjVzNGkgIZwiCKXqk5MtqKfB2Wp_DG58SzfM2UmPJJpeIbflU-D0_xxCnD0u0En_OccmeRsSEE56KY5q_NtQtnpwUekEvtvEjhIiGfU0M5NdgljLvnAN7JkPvv-Kjd2liy59j17QaCgYKAWkSARASFQHGX2MiA14X1b8pOFBNNRtkZ3xKug0177", + "expires_in": 3599, + "scope": "https://www.googleapis.com/auth/calendar", + "token_type": "Bearer", + "refresh_token_expires_in": 571598, + "refresh_token": "1//0erCBDblHqeswCgYIARAAGA4SNwF-L9IrKNzH7uKvucIavo21bciIQe04jXA3w-aHCLvrUmUNoaIVNn1wx4fapAmpifxS-kjcPyk" +} \ No newline at end of file