From a53aae4fd31d3dd38e6cd523650753d3b1d0e091 Mon Sep 17 00:00:00 2001 From: za0012 Date: Fri, 12 Jun 2026 16:14:37 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=99=95=EC=9E=A5=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EA=B7=B8=EB=9E=A8=20=ED=86=B5=EA=B3=BC=20?= =?UTF-8?q?=EB=90=98=EC=96=B4=EC=95=BC=EB=A7=8C=20=EC=98=AC=EB=A6=B4=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extension/content.css | 218 ++++++++++++++++++++++-------- extension/content.js | 299 ++++++++++++++++++++++++++++++++++++++++-- extension/popup.css | 119 +++++++++++------ 3 files changed, 530 insertions(+), 106 deletions(-) diff --git a/extension/content.css b/extension/content.css index 386025c..910a201 100644 --- a/extension/content.css +++ b/extension/content.css @@ -1,39 +1,114 @@ #codetest-study-uploader-open { position: fixed; - right: 22px; - bottom: 22px; + right: 16px; + bottom: 16px; z-index: 2147483646; border: 0; - border-radius: 999px; - background: #2563eb; + border-radius: 16px; + background: #3182f6; color: #fff; cursor: pointer; font: - 700 13px / 1 system-ui, + 800 13px / 1 "Pretendard Variable", + Pretendard, + system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - padding: 12px 16px; - box-shadow: 0 12px 30px rgba(37, 99, 235, 0.28); + padding: 14px 18px; + box-shadow: 0 14px 34px rgba(49, 130, 246, 0.3); + touch-action: none; + transition: + background 0.2s ease, + box-shadow 0.2s ease, + transform 0.2s ease; + user-select: none; +} + +#codetest-study-uploader-open:hover { + background: #1b64da; +} + +#codetest-study-uploader-open[data-dragging="true"] { + cursor: grabbing; + transition: none; + transform: scale(1.03); +} + +#codetest-study-uploader-open[data-corner="top-left"] { + top: 16px; + right: auto; + bottom: auto; + left: 16px; +} + +#codetest-study-uploader-open[data-corner="top-right"] { + top: 16px; + right: 16px; + bottom: auto; + left: auto; +} + +#codetest-study-uploader-open[data-corner="bottom-left"] { + top: auto; + right: auto; + bottom: 16px; + left: 16px; +} + +#codetest-study-uploader-open[data-corner="bottom-right"] { + top: auto; + right: 16px; + bottom: 16px; + left: auto; } #codetest-study-uploader-panel { position: fixed; - right: 22px; - bottom: 78px; + right: 16px; + bottom: 82px; z-index: 2147483647; - width: min(360px, calc(100vw - 32px)); + width: min(420px, calc(100vw - 16px)); box-sizing: border-box; - border: 1px solid #d9e2f1; - border-radius: 8px; + border: 1px solid #edf1f7; + border-radius: 26px; background: #fff; - color: #172033; + color: #191f28; display: none; - padding: 18px; - box-shadow: 0 24px 60px rgba(15, 23, 42, 0.22); + padding: 30px; + box-shadow: 0 18px 48px rgba(25, 31, 40, 0.16); font-family: - system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + "Pretendard Variable", Pretendard, system-ui, -apple-system, + BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +#codetest-study-uploader-panel[data-corner="top-left"] { + top: 82px; + right: auto; + bottom: auto; + left: 16px; +} + +#codetest-study-uploader-panel[data-corner="top-right"] { + top: 82px; + right: 16px; + bottom: auto; + left: auto; +} + +#codetest-study-uploader-panel[data-corner="bottom-left"] { + top: auto; + right: auto; + bottom: 82px; + left: 16px; +} + +#codetest-study-uploader-panel[data-corner="bottom-right"] { + top: auto; + right: 16px; + bottom: 82px; + left: auto; } #codetest-study-uploader-panel[data-open="true"] { @@ -48,64 +123,87 @@ display: flex; align-items: flex-start; justify-content: space-between; - gap: 12px; - margin-bottom: 14px; + gap: 14px; + margin-bottom: 25px; +} + +.ctsu-header > div { + min-width: 0; + padding-left: 11px; + border-left: 4px solid #3182f6; } .ctsu-eyebrow { - margin: 0 0 4px; - color: #2563eb; + margin: 0 0 5px; + color: #3182f6; font-size: 11px; - font-weight: 800; + font-weight: 900; letter-spacing: 0; text-transform: uppercase; } .ctsu-title { display: block; - max-width: 270px; + max-width: 326px; overflow: hidden; - color: #111827; - font-size: 16px; + color: #191f28; + font-size: 20px; + font-weight: 800; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; } +.ctsu-subtitle { + display: block; + margin-top: 6px; + color: #8b95a1; + font-size: 13px; + font-weight: 600; + line-height: 1.45; +} + .ctsu-close { - width: 28px; - height: 28px; + width: 34px; + height: 34px; border: 0; - border-radius: 6px; - background: #f3f6fb; - color: #607086; + border-radius: 999px; + background: #f2f4f6; + color: #8b95a1; cursor: pointer; - font-size: 20px; + font-size: 22px; line-height: 1; } +.ctsu-close:hover { + background: #e5e8eb; + color: #4e5968; +} + .ctsu-field { display: block; - margin-top: 12px; + margin-top: 17px; } .ctsu-field span { display: block; - margin-bottom: 7px; - color: #607086; + margin-bottom: 8px; + color: #6b7684; font-size: 12px; - font-weight: 700; + font-weight: 800; } .ctsu-difficulty, .ctsu-memo { width: 100%; - border: 1px solid #d9e2f1; - border-radius: 7px; - background: #f8fafc; - color: #172033; + border: 0; + border-radius: 16px; + background: #f8f9fb; + color: #191f28; font: - 500 14px / 1.45 system-ui, + 600 14px / 1.5 "Pretendard Variable", + Pretendard, + system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", @@ -114,52 +212,64 @@ } .ctsu-difficulty { - height: 38px; - padding: 0 10px; + height: 50px; + padding: 0 16px; } .ctsu-memo { - min-height: 112px; - padding: 10px; + min-height: 155px; + padding: 17px; resize: vertical; } .ctsu-difficulty:focus, .ctsu-memo:focus { - border-color: #2563eb; background: #fff; - box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12); + box-shadow: 0 0 0 3px rgba(49, 130, 246, 0.16); } .ctsu-status { - min-height: 18px; - margin: 12px 0; - color: #607086; + min-height: 46px; + margin: 18px 0; + border-radius: 12px; + background: #f2f4f6; + color: #6b7684; font-size: 12px; + font-weight: 700; line-height: 1.45; + padding: 12px 14px; } .ctsu-status[data-variant="error"] { + background: #fff1f2; color: #dc2626; } .ctsu-status[data-variant="success"] { + background: #eefbf4; color: #15803d; } .ctsu-submit { width: 100%; - height: 42px; + height: 54px; border: 0; - border-radius: 7px; - background: #2563eb; + border-radius: 16px; + background: #3182f6; color: #fff; cursor: pointer; - font-size: 14px; - font-weight: 800; + font-size: 15px; + font-weight: 900; + box-shadow: 0 10px 22px rgba(49, 130, 246, 0.22); +} + +.ctsu-submit:hover { + background: #1b64da; } .ctsu-submit:disabled { + background: #d4dbe6; + box-shadow: none; + color: #8b95a1; cursor: not-allowed; - opacity: 0.64; } diff --git a/extension/content.js b/extension/content.js index 31a115a..3bfc973 100644 --- a/extension/content.js +++ b/extension/content.js @@ -1,7 +1,18 @@ const PROGRAMMERS_LEVELS = ["Lv.0", "Lv.1", "Lv.2", "Lv.3", "Lv.4", "Lv.5"]; +const DEFAULT_CORNER = "bottom-right"; +const DIFFICULTY_CACHE_PREFIX = "programmersDifficulty:"; +const FLOATING_CORNERS = new Set([ + "top-left", + "top-right", + "bottom-left", + "bottom-right", +]); + let panel; let observer; let autoOpened = false; +let acceptedDetected = false; +let currentCorner = DEFAULT_CORNER; function normalizeText(value) { return value?.replace(/\s+/g, " ").trim() || ""; @@ -34,12 +45,140 @@ function getProblemTags() { return Array.from(new Set(candidates)).slice(0, 5); } -function getDifficultyFromPage() { - const text = normalizeText(document.body.innerText); - const match = text.match(/Lv\.?\s*([0-5])/i); +function parseDifficultyFromText(text) { + const normalizedText = normalizeText(text); + const match = + normalizedText.match(/Lv\.?\s*([0-5])/i) || + normalizedText.match(/level["'\s:=]+([0-5])/i) || + normalizedText.match(/difficulty["'\s:=]+(?:Lv\.?)?\s*([0-5])/i); + return match ? `Lv.${match[1]}` : "Lv.0"; } +function getLessonId() { + return location.pathname.match(/\/lessons\/(\d+)/)?.[1] ?? ""; +} + +function findDifficultyNearLessonLink(html, lessonId) { + if (!lessonId) { + return "Lv.0"; + } + + const lessonIndex = html.indexOf(`/lessons/${lessonId}`); + if (lessonIndex === -1) { + return "Lv.0"; + } + + const nearbyText = html.slice( + Math.max(0, lessonIndex - 3000), + Math.min(html.length, lessonIndex + 3000), + ); + + return parseDifficultyFromText(nearbyText); +} + +function getDifficultyFromPage() { + const candidates = [ + document.body.innerText, + document.documentElement.innerHTML, + ...Array.from(document.querySelectorAll("meta, script")) + .map((element) => element.getAttribute("content") || element.textContent) + .filter(Boolean), + ]; + + for (const candidate of candidates) { + const difficulty = parseDifficultyFromText(candidate); + if (difficulty !== "Lv.0") { + return difficulty; + } + } + + return "Lv.0"; +} + +async function getCachedDifficulty(lessonId) { + if (!lessonId) { + return ""; + } + + const cacheKey = `${DIFFICULTY_CACHE_PREFIX}${lessonId}`; + const cached = await chrome.storage.local.get(cacheKey); + return cached[cacheKey] || ""; +} + +function setCachedDifficulty(lessonId, difficulty) { + if (!lessonId || !difficulty || difficulty === "Lv.0") { + return; + } + + chrome.storage.local.set({ + [`${DIFFICULTY_CACHE_PREFIX}${lessonId}`]: difficulty, + }); +} + +async function fetchDifficultyFromChallengeSearch(title, lessonId) { + if (!title || !lessonId) { + return "Lv.0"; + } + + const encodedTitle = encodeURIComponent(title); + const searchPaths = [ + `/learn/challenges?order=recent&search=${encodedTitle}`, + `/learn/challenges?order=acceptance_desc&search=${encodedTitle}`, + `/learn/challenges?order=level_asc&search=${encodedTitle}`, + ]; + + for (const path of searchPaths) { + const response = await fetch(path, { credentials: "include" }); + const html = await response.text(); + const difficulty = findDifficultyNearLessonLink(html, lessonId); + + if (difficulty !== "Lv.0") { + return difficulty; + } + } + + for (const level of [0, 1, 2, 3, 4, 5]) { + const response = await fetch( + `/learn/challenges?order=recent&levels=${level}&search=${encodedTitle}`, + { credentials: "include" }, + ); + const html = await response.text(); + + if (html.includes(`/lessons/${lessonId}`)) { + return `Lv.${level}`; + } + } + + return "Lv.0"; +} + +async function detectDifficulty() { + const lessonId = getLessonId(); + const pageDifficulty = getDifficultyFromPage(); + + if (pageDifficulty !== "Lv.0") { + setCachedDifficulty(lessonId, pageDifficulty); + return pageDifficulty; + } + + const cachedDifficulty = await getCachedDifficulty(lessonId); + if (cachedDifficulty) { + return cachedDifficulty; + } + + try { + const fetchedDifficulty = await fetchDifficultyFromChallengeSearch( + getProblemTitle(), + lessonId, + ); + setCachedDifficulty(lessonId, fetchedDifficulty); + return fetchedDifficulty; + } catch { + return "Lv.0"; + } +} + function getProblemUrl() { return `${location.origin}${location.pathname}`; } @@ -110,6 +249,110 @@ function looksAccepted() { ); } +function updateAcceptedState() { + if (looksAccepted()) { + acceptedDetected = true; + } + + return acceptedDetected; +} + +function isFloatingCorner(value) { + return FLOATING_CORNERS.has(value); +} + +function nearestCorner(clientX, clientY) { + const vertical = clientY < window.innerHeight / 2 ? "top" : "bottom"; + const horizontal = clientX < window.innerWidth / 2 ? "left" : "right"; + return `${vertical}-${horizontal}`; +} + +function applyFloatingCorner(corner) { + currentCorner = isFloatingCorner(corner) ? corner : DEFAULT_CORNER; + + const button = document.getElementById("codetest-study-uploader-open"); + if (button) { + button.dataset.corner = currentCorner; + button.style.left = ""; + button.style.right = ""; + button.style.top = ""; + button.style.bottom = ""; + } + + if (panel) { + panel.dataset.corner = currentCorner; + } +} + +async function restoreFloatingCorner() { + const { uploaderCorner } = await chrome.storage.local.get("uploaderCorner"); + applyFloatingCorner(uploaderCorner); +} + +function saveFloatingCorner(corner) { + chrome.storage.local.set({ uploaderCorner: corner }); +} + +function enableFloatingButtonDrag(button) { + let startX = 0; + let startY = 0; + let moved = false; + + button.addEventListener("pointerdown", (event) => { + startX = event.clientX; + startY = event.clientY; + moved = false; + button.dataset.dragging = "true"; + button.setPointerCapture(event.pointerId); + }); + + button.addEventListener("pointermove", (event) => { + if (button.dataset.dragging !== "true") { + return; + } + + const distance = Math.hypot(event.clientX - startX, event.clientY - startY); + if (distance < 6 && !moved) { + return; + } + + moved = true; + const rect = button.getBoundingClientRect(); + const margin = 14; + const left = Math.min( + Math.max(event.clientX - rect.width / 2, margin), + window.innerWidth - rect.width - margin, + ); + const top = Math.min( + Math.max(event.clientY - rect.height / 2, margin), + window.innerHeight - rect.height - margin, + ); + + button.style.left = `${left}px`; + button.style.top = `${top}px`; + button.style.right = "auto"; + button.style.bottom = "auto"; + }); + + button.addEventListener("pointerup", (event) => { + if (button.dataset.dragging !== "true") { + return; + } + + button.dataset.dragging = "false"; + button.releasePointerCapture(event.pointerId); + + if (!moved) { + return; + } + + const corner = nearestCorner(event.clientX, event.clientY); + button.dataset.dragged = "true"; + applyFloatingCorner(corner); + saveFloatingCorner(corner); + }); +} + function createFloatingButton() { if (document.getElementById("codetest-study-uploader-open")) { return; @@ -119,8 +362,18 @@ function createFloatingButton() { button.id = "codetest-study-uploader-open"; button.type = "button"; button.textContent = "스터디 업로드"; - button.addEventListener("click", () => openPanel(false)); + button.addEventListener("click", (event) => { + if (button.dataset.dragged === "true") { + event.preventDefault(); + button.dataset.dragged = "false"; + return; + } + + openPanel(false); + }); document.body.appendChild(button); + enableFloatingButtonDrag(button); + restoreFloatingCorner(); } function createPanel() { @@ -131,6 +384,7 @@ function createPanel() {

Programmers

+ 통과한 문제만 스터디에 올릴 수 있어요.
@@ -162,22 +416,32 @@ async function openPanel(fromAccepted) { panel = createPanel(); } + if (fromAccepted) { + acceptedDetected = true; + } + const auth = await chrome.runtime.sendMessage({ type: "GET_AUTH_STATUS" }); + const canUpload = updateAcceptedState(); panel.dataset.open = "true"; + panel.dataset.ready = String(canUpload); + panel.dataset.corner = currentCorner; panel.querySelector(".ctsu-title").textContent = getProblemTitle(); - panel.querySelector(".ctsu-difficulty").value = getDifficultyFromPage(); + panel.querySelector(".ctsu-difficulty").value = await detectDifficulty(); const status = panel.querySelector(".ctsu-status"); + const submitButton = panel.querySelector(".ctsu-submit"); if (!auth?.loggedIn) { + submitButton.disabled = true; status.textContent = "확장 프로그램 아이콘을 눌러 먼저 로그인해주세요."; status.dataset.variant = "error"; return; } - status.textContent = fromAccepted - ? "통과가 감지됐어요. 메모만 적고 올리면 됩니다." - : "메모를 적고 업로드할 수 있습니다."; - status.dataset.variant = "idle"; + submitButton.disabled = !canUpload; + status.textContent = canUpload + ? "통과가 확인됐어요. 메모를 적고 업로드할 수 있습니다." + : "아직 통과가 확인되지 않았어요. 통과 후 업로드할 수 있습니다."; + status.dataset.variant = canUpload ? "success" : "idle"; } function closePanel() { @@ -190,13 +454,24 @@ async function submitProblem() { const submitButton = panel.querySelector(".ctsu-submit"); const status = panel.querySelector(".ctsu-status"); + if (!updateAcceptedState()) { + submitButton.disabled = true; + panel.dataset.ready = "false"; + status.textContent = "문제가 통과된 뒤에만 업로드할 수 있습니다."; + status.dataset.variant = "error"; + return; + } + submitButton.disabled = true; status.textContent = "코드와 문제 정보를 읽는 중입니다..."; status.dataset.variant = "idle"; const code = await readCodeFromPage(); + const selectedDifficulty = panel.querySelector(".ctsu-difficulty").value; + const detectedDifficulty = await detectDifficulty(); const payload = { - difficulty: panel.querySelector(".ctsu-difficulty").value, + difficulty: + detectedDifficulty !== "Lv.0" ? detectedDifficulty : selectedDifficulty, memo: panel.querySelector(".ctsu-memo").value, solution: code, tags: getProblemTags(), @@ -225,7 +500,7 @@ async function submitProblem() { function startAcceptedObserver() { observer = new MutationObserver(() => { - if (!autoOpened && looksAccepted()) { + if (!autoOpened && updateAcceptedState()) { autoOpened = true; openPanel(true); } @@ -237,7 +512,7 @@ function startAcceptedObserver() { }); window.setTimeout(() => { - if (!autoOpened && looksAccepted()) { + if (!autoOpened && updateAcceptedState()) { autoOpened = true; openPanel(true); } diff --git a/extension/popup.css b/extension/popup.css index c6cd517..75c1c1a 100644 --- a/extension/popup.css +++ b/extension/popup.css @@ -1,74 +1,99 @@ body { - width: 320px; + width: 335px; margin: 0; - background: #f8fafc; - color: #111827; + background: #f2f4f6; + color: #191f28; font-family: - system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + "Pretendard Variable", Pretendard, system-ui, -apple-system, + BlinkMacSystemFont, "Segoe UI", sans-serif; } main { - padding: 18px; + margin: 0; + background: #fff; + padding: 24px; } header { - margin-bottom: 18px; + position: relative; + margin-bottom: 22px; + padding-left: 12px; +} + +header::before { + position: absolute; + top: 2px; + left: 0; + width: 4px; + height: 24px; + border-radius: 999px; + background: #3182f6; + content: ""; } header strong { display: block; - font-size: 16px; + color: #191f28; + font-size: 18px; + font-weight: 900; line-height: 1.3; } header p { - margin: 5px 0 0; - color: #64748b; + margin: 6px 0 0; + color: #8b95a1; font-size: 12px; + font-weight: 600; line-height: 1.45; } label { display: block; - margin-bottom: 12px; + margin-bottom: 14px; } label span { display: block; - margin-bottom: 6px; - color: #475569; + margin-bottom: 8px; + color: #6b7684; font-size: 12px; - font-weight: 700; + font-weight: 800; } input { width: 100%; - height: 38px; + height: 45px; box-sizing: border-box; - border: 1px solid #cbd5e1; - border-radius: 7px; - background: #fff; - color: #111827; - font-size: 13px; + border: 0; + border-radius: 16px; + background: #f8f9fb; + color: #191f28; + font-size: 14px; + font-weight: 600; outline: none; - padding: 0 10px; + padding: 0 15px; } input:focus { - border-color: #2563eb; - box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12); + background: #fff; + box-shadow: 0 0 0 3px rgba(49, 130, 246, 0.16); } button { width: 100%; - height: 40px; + height: 48px; border: 0; - border-radius: 7px; - background: #2563eb; + border-radius: 16px; + background: #3182f6; color: #fff; cursor: pointer; - font-size: 13px; - font-weight: 800; + font-size: 15px; + font-weight: 900; + box-shadow: 0 10px 22px rgba(49, 130, 246, 0.2); +} + +button:hover { + background: #1b64da; } button:disabled { @@ -77,43 +102,57 @@ button:disabled { } #logout { - margin-top: 12px; - background: #e2e8f0; - color: #1e293b; + margin-top: 14px; + background: #f2f4f6; + box-shadow: none; + color: #4e5968; +} + +#logout:hover { + background: #e5e8eb; } .account { - border: 1px solid #dbe3ee; - border-radius: 8px; - background: #fff; - padding: 12px; + border: 0; + border-radius: 16px; + background: #f8f9fb; + padding: 15px; } .account span { display: block; - color: #64748b; + color: #8b95a1; font-size: 12px; + font-weight: 700; line-height: 1.35; } .account strong { display: block; - margin-top: 4px; - font-size: 15px; + margin-top: 5px; + color: #191f28; + font-size: 16px; + font-weight: 900; } #status { - min-height: 18px; - margin: 12px 0 0; - color: #64748b; + min-height: 38px; + margin: 15px 0 0; + border-radius: 10px; + background: #f2f4f6; + color: #6b7684; font-size: 12px; + font-weight: 700; line-height: 1.45; + padding: 10px 12px; } #status[data-variant="error"] { + background: #fff1f2; color: #dc2626; } #status[data-variant="success"] { + background: #eefbf4; color: #15803d; } From a54f29668a74aec30b4f0f3f0c7e98d076d1ae98 Mon Sep 17 00:00:00 2001 From: za0012 Date: Fri, 12 Jun 2026 17:20:10 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EC=A3=BC=EC=84=9D=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0,?= =?UTF-8?q?=20=EC=9E=84=EC=9D=98=EC=9D=98=20=EA=B0=92=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=84=A3=EC=9D=80=20=EB=B6=80=EB=B6=84=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=ED=95=A0=EB=8B=B9=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD,=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=EC=9D=B4=20=EC=97=AC=EC=A0=84=ED=9E=88=20?= =?UTF-8?q?=EB=82=A8=EC=95=84=EC=9E=88=EB=8A=94=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StudySetting/modal/DeleteStudyConfirmModal.tsx | 7 ------- .../components/StudySetting/modal/EditStudyModal.tsx | 9 ++++++--- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/app/(with-sidebar)/settings/components/StudySetting/modal/DeleteStudyConfirmModal.tsx b/src/app/(with-sidebar)/settings/components/StudySetting/modal/DeleteStudyConfirmModal.tsx index fcd0241..8d77c50 100644 --- a/src/app/(with-sidebar)/settings/components/StudySetting/modal/DeleteStudyConfirmModal.tsx +++ b/src/app/(with-sidebar)/settings/components/StudySetting/modal/DeleteStudyConfirmModal.tsx @@ -12,25 +12,18 @@ export function DeleteStudyConfirmModal({ }: deleteStudyModalProp) { return (
- {/* 1. 이모지 대신 세련된 Lucide 아이콘 배치 (토스 경고용 소프트 핑크/레드 배경) */}
- - {/* 2. 타이틀: 폰트 크기를 더 키우고 글자색을 아주 깊고 선명한 네이비/블랙 계열로 변경 */}

스터디를 삭제할까요?

- - {/* 3. 설명문: 폰트 크기와 자간을 조절하고, '다시는 복구할 수 없어요'를 강조 */}

삭제된 스터디 정보는
다시는 복구할 수 없어요.

- - {/* 4. 버튼: 높이를 더 넓히고 자간(tracking-wide)과 부드러운 텍스트 색상 매칭 */}
+
+ ); +} diff --git a/src/app/(with-sidebar)/settings/components/AccountSetting/service.ts b/src/app/(with-sidebar)/settings/components/AccountSetting/service.ts index e69de29..4087f64 100644 --- a/src/app/(with-sidebar)/settings/components/AccountSetting/service.ts +++ b/src/app/(with-sidebar)/settings/components/AccountSetting/service.ts @@ -0,0 +1,24 @@ +import { supabase } from "@/lib/supabase"; + +// 스터디 탈퇴 +export const leaveStudy = async (studyId: number) => { + const user = (await supabase.auth.getUser()).data.user; + if (!user) throw new Error("로그인 필요"); + + const { data: member, error } = await supabase + .from("study_members") + .select("role") + .eq("study_id", studyId) + .eq("user_id", user.id) + .single(); + if (error) throw error; + if (member.role === "스터디장") + throw new Error("스터디장은 위임 후 탈퇴할 수 있어요"); + + const { error: deleteError } = await supabase + .from("study_members") + .delete() + .eq("study_id", studyId) + .eq("user_id", user.id); + if (deleteError) throw deleteError; +}; diff --git a/src/app/(with-sidebar)/settings/components/AccountSetting/view.tsx b/src/app/(with-sidebar)/settings/components/AccountSetting/view.tsx index 7f12703..b85475c 100644 --- a/src/app/(with-sidebar)/settings/components/AccountSetting/view.tsx +++ b/src/app/(with-sidebar)/settings/components/AccountSetting/view.tsx @@ -1,6 +1,47 @@ +import { useQuery } from "@tanstack/react-query"; +import { useSetAtom } from "jotai"; import { ChevronRight, LogOut, Trash2 } from "lucide-react"; +import { getMyMemberInfo } from "@/lib/api/study"; +import { alertAtom } from "@/lib/store/alertStore"; +import { modalAtom } from "@/lib/store/modalStore"; +import type { UserProfile } from "@/lib/types/study"; +import { ExitStudy } from "./modal/ExitStudy"; +import { leaveStudy } from "./service"; function AccountSetting() { + const setModal = useSetAtom(modalAtom); + const setAlert = useSetAtom(alertAtom); + + const { data: myInfo } = useQuery({ + queryKey: ["myInfoInAccountSetting"], + queryFn: getMyMemberInfo, + }); + const checkExitStudy = () => { + if (!myInfo?.id) return; + + setModal({ + title: "", + children: ( + setModal(null)} + onConfirm={() => exitStudy(myInfo.study_id)} + /> + ), + }); + }; + const exitStudy = (id: number) => { + try { + leaveStudy(id); + } catch (error) { + setAlert({ + title: " 실패", + content: "스터디 정보가 존재하지 않습니다", + variant: true, + }); + console.log(error); + } + }; + return (

@@ -28,6 +69,7 @@ function AccountSetting() {