From ee5205e133c12ecbe40160f9abc57d129c892c5c Mon Sep 17 00:00:00 2001 From: root Date: Tue, 22 Sep 2026 15:45:55 +0900 Subject: [PATCH] =?UTF-8?q?SSO=20=EC=A4=91=EB=B3=B5=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=20=EC=98=A4=EB=A5=98=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/styles.css | 17 +++++++++++++---- egbim/app.js | 7 ++++++- egbim/detail.html | 2 +- src/index.js | 20 +++++++++++++------- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/assets/styles.css b/assets/styles.css index d27d4df..f784a3c 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -266,6 +266,7 @@ button { cursor: pointer; } .detail-head { padding: 23px 22px 20px; border-bottom: 1px solid var(--line); } .detail-title-line { display: flex; align-items: center; justify-content: space-between; gap: 16px; } .detail-title-line h2 { margin: 0; font-size: 22px; letter-spacing: -.04em; } +.detail-title-line .status { min-width: 82px; padding: 5px 10px; font-size: 13px; font-weight: 700; } .detail-meta { display: flex; gap: 14px; flex-wrap: wrap; padding-top: 15px; color: #888; font-size: 13px; } .detail-meta span + span::before { content: ''; display: inline-block; width: 1px; height: 11px; margin: 0 14px 0 0; background: #ddd; vertical-align: -1px; } .detail-body { min-height: 260px; padding: 38px 22px 56px; white-space: pre-wrap; word-break: break-word; } @@ -274,6 +275,8 @@ button { cursor: pointer; } .attachment-item { padding: 7px 0; color: #555; font-size: 13px; } .attachments a { color: var(--navy); text-decoration: underline; } .detail-actions { display: flex; justify-content: space-between; padding: 23px 22px 0; } +.egbim-page #backButton { display: none; } +.egbim-page .form-footer:has(#backButton) { display: none; } .owner-actions { display: flex; gap: 8px; } .button-danger { border-color: #c43d3d; background: #c43d3d; color: #fff; } .button-danger:hover { border-color: #a52f2f; background: #a52f2f; } @@ -285,11 +288,12 @@ button { cursor: pointer; } .comment-meta { display: flex; justify-content: space-between; margin-bottom: 7px; color: #777; font-size: 13px; } .comment-content { white-space: pre-wrap; } .comment-empty { padding: 32px; border-bottom: 1px solid var(--line); color: #999; text-align: center; } -.comment-form { padding: 20px 10px 0; } +.comment-form { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 16px; padding: 20px 10px 0; } .comment-form .textarea { min-height: 120px; } -.comment-attachment-input { padding-top: 14px; } +.comment-form > .textarea { grid-column: 1 / -1; } +.comment-attachment-input { grid-column: 1; padding-top: 14px; } .comment-attachment-input input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; clip-path: inset(50%); } -.comment-image-button { height: 34px; padding: 0 13px; font-size: 13px; } +.comment-image-button { height: 42px; padding: 0 21px; font-size: 13px; } .comment-image-preview, .comment-attachments-list { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; } .comment-image-preview-item { width: 104px; overflow: hidden; border: 1px solid #ddd; background: #fafafa; } .comment-image-preview-item img { display: block; width: 104px; height: 78px; object-fit: cover; } @@ -297,8 +301,11 @@ button { cursor: pointer; } .comment-image-link { display: block; width: 124px; } .comment-image { display: block; width: 124px; height: 94px; border: 1px solid #ddd; object-fit: cover; } .comment-attachment-name { color: #666; font-size: 13px; } -.comment-form-actions { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding-top: 10px; } +.comment-form-actions { display: flex; grid-column: 2; align-items: flex-end; justify-content: flex-end; gap: 12px; padding-top: 14px; } .comment-form-actions .form-error { display: block; flex: 1; margin: 0; } +.comment-form-actions .form-error[hidden] { display: none; } +#commentSubmit { height: 42px; border-color: #000; background: #000; color: #fff; } +#commentSubmit:hover { border-color: #222; background: #222; } .site-footer { padding: 38px 32px; background: #111; color: #b9b9b9; } .footer-inner { display: flex; align-items: flex-start; justify-content: space-between; max-width: 1280px; margin: 0 auto; gap: 40px; } @@ -436,6 +443,8 @@ button { cursor: pointer; } .detail-actions .button { flex: 1; padding: 0 10px; } .footer-inner { display: block; } .footer-links { margin-top: 18px; } + .comment-form { grid-template-columns: minmax(0, 1fr); } + .comment-form-actions { grid-column: 1; justify-content: flex-end; } } @media (max-width: 480px) { diff --git a/egbim/app.js b/egbim/app.js index e98b433..b82ef2b 100644 --- a/egbim/app.js +++ b/egbim/app.js @@ -405,6 +405,10 @@ function initDetail() { const detail = qs('#detail'); if (!detail) return; + const detailListLink = qs('.detail-actions > a', detail); + if (detailListLink) detailListLink.textContent = '목록으로'; + const bottomBackButton = qs('#backButton', detail); + if (bottomBackButton && bottomBackButton.parentElement) bottomBackButton.parentElement.remove(); const id = new URLSearchParams(window.location.search).get('id') || 'EG-1048'; const post = getPosts().find(function (item) { return item.id === id; }); if (!post) { @@ -596,6 +600,8 @@ }); const commentImages = qs('#commentImages'); const commentImagePreview = qs('#commentImagePreview'); + const commentImageLabel = qs('label[for="commentImages"]'); + if (commentImageLabel) commentImageLabel.textContent = '댓글 이미지 첨부'; if (commentImages && commentImagePreview) commentImages.addEventListener('change', function () { const files = Array.prototype.slice.call(commentImages.files || []); commentImagePreview.innerHTML = files.filter(function (file) { return /^image\//i.test(file.type); }).map(function (file) { @@ -606,7 +612,6 @@ refreshRemoteStatus(post).then(function (changed) { if (changed) qs('[data-detail-status]').textContent = normalizeStatus(post.status) || '공지'; }); - qs('#backButton').addEventListener('click', function () { window.location.href = 'index.html'; }); } document.addEventListener('DOMContentLoaded', function () { bindGlobal(); initList(); initWrite(); initDetail(); }); diff --git a/egbim/detail.html b/egbim/detail.html index 8d776c9..11b9602 100644 --- a/egbim/detail.html +++ b/egbim/detail.html @@ -3,6 +3,6 @@ 문의 상세 | EG-BIM Q&A
EG-BIM SUPPORT

Q&A

EG-BIM 관련 문의하기

-

문의 상세

문의 내용과 답변을 확인할 수 있습니다.

문의 문의 제목

문의접수
2026-09-17작성자회사부서

첨부파일

첨부된 파일이 없습니다.
목록

답변 0

+

문의 상세

문의 내용과 답변을 확인할 수 있습니다.

문의 문의 제목

문의접수
2026-09-17작성자회사부서

첨부파일

첨부된 파일이 없습니다.
목록

답변 0

diff --git a/src/index.js b/src/index.js index ceaa4bb..78cb68d 100644 --- a/src/index.js +++ b/src/index.js @@ -96,6 +96,8 @@ function redirect(url, headers = {}) { else responseHeaders.set(name, value); }); responseHeaders.set('location', url); + responseHeaders.set('cache-control', 'no-store, no-cache, must-revalidate'); + responseHeaders.set('pragma', 'no-cache'); return new Response(null, { status: 302, headers: responseHeaders }); } @@ -250,32 +252,36 @@ async function startLogin(request, env) { return redirect(authorize.toString(), { 'set-cookie': cookie(env.OAUTH_COOKIE_NAME, signedState, 600) }); } +function oauthError(env, error, detail, status) { + return json({ error, detail: detail || '' }, status, { 'set-cookie': cookie(env.OAUTH_COOKIE_NAME, '', 0, { delete: true }) }); +} + async function finishLogin(request, env) { const missing = requiredAuthConfig(env); if (missing.length) return json({ error: 'auth_not_configured', missing }, 503); const url = new URL(request.url); const signedState = getCookie(request, env.OAUTH_COOKIE_NAME); const statePayload = await verifyValue(signedState, env.SESSION_SECRET); - if (!statePayload) return json({ error: 'invalid_oauth_state' }, 400); + if (!statePayload) return oauthError(env, 'invalid_oauth_state', '', 400); let state; - try { state = JSON.parse(decoder.decode(fromBase64url(statePayload))); } catch (error) { return json({ error: 'invalid_oauth_state' }, 400); } - if (url.searchParams.get('state') !== state.state || Date.now() - state.createdAt > 10 * 60 * 1000) return json({ error: 'invalid_oauth_state' }, 400); + try { state = JSON.parse(decoder.decode(fromBase64url(statePayload))); } catch (error) { return oauthError(env, 'invalid_oauth_state', '', 400); } + if (url.searchParams.get('state') !== state.state || Date.now() - state.createdAt > 10 * 60 * 1000) return oauthError(env, 'invalid_oauth_state', '', 400); const code = url.searchParams.get('code'); - if (!code) return json({ error: 'oauth_code_missing', detail: url.searchParams.get('error_description') || url.searchParams.get('error') || '' }, 400); + if (!code) return oauthError(env, 'oauth_code_missing', url.searchParams.get('error_description') || url.searchParams.get('error') || '', 400); const tokenParams = new URLSearchParams({ grant_type: 'authorization_code', client_id: env.AUTH_CLIENT_ID, redirect_uri: env.AUTH_REDIRECT_URI, code, code_verifier: state.verifier }); const tokenResponse = await fetch(env.AUTH_TOKEN_URL, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' }, body: tokenParams }); const tokens = await tokenResponse.json().catch(() => ({})); - if (!tokenResponse.ok) return json({ error: 'oauth_token_exchange_failed', detail: tokens.error_description || tokens.error || '' }, 502); + if (!tokenResponse.ok) return oauthError(env, 'oauth_token_exchange_failed', tokens.error_description || tokens.error || '', 502); let claims = parseJwt(tokens.id_token || tokens.access_token); - if (state.nonce && claims.nonce && state.nonce !== claims.nonce) return json({ error: 'invalid_oauth_nonce' }, 400); + if (state.nonce && claims.nonce && state.nonce !== claims.nonce) return oauthError(env, 'invalid_oauth_nonce', '', 400); if (env.AUTH_USERINFO_URL && tokens.access_token) { const userResponse = await fetch(env.AUTH_USERINFO_URL, { headers: { authorization: `Bearer ${tokens.access_token}`, accept: 'application/json' } }); const userInfo = await userResponse.json().catch(() => ({})); if (userResponse.ok) claims = { ...claims, ...userInfo }; } const user = normalizeClaims(claims); - if (!user.ssoSubject) return json({ error: 'sso_subject_missing' }, 502); + if (!user.ssoSubject) return oauthError(env, 'sso_subject_missing', '', 502); const sessionPayload = base64url(JSON.stringify({ user, expiresAt: Date.now() + Number(env.SESSION_TTL_SECONDS || 3600) * 1000 })); const session = await signValue(sessionPayload, env.SESSION_SECRET); return redirect(new URL(state.returnUrl || '/', url.origin).toString(), { 'set-cookie': [cookie(env.SESSION_COOKIE_NAME, session, Number(env.SESSION_TTL_SECONDS || 3600)), cookie(env.OAUTH_COOKIE_NAME, '', 0, { delete: true })] });