
File name
Commit message
Commit date
05-22
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
<template>
<div class="login-page page" :class="{ 'loading-blur': isLoading }" :style="{ 'cursor': isLoading ? 'wait' : '' }">
<div>
<div class="background-img">
<p>
어서오세요.<br />
로그인하세요.
</p>
</div>
<!-- 일반 로그인 화면 -->
<div class="login-wrap" v-if="!loginStep1 && !loginStep2">
<div class="login">
<div :class="{ 'login-title': true, 'user-login': !isAdminPage }">
<router-link :to="{ path: $filters.ctxPath('/') }">LOGIN(홈으로 이동)</router-link>
</div>
<!-- 아이디/비밀번호 입력 -->
<div class="form-group">
<label for="id" class="login-label">아이디</label>
<input type="text" id="id" class="form-control md" placeholder="아이디를 입력하세요" v-model="member.lgnId" />
</div>
<div class="form-group">
<label for="pw" class="login-label">비밀번호</label>
<input type="password" id="pw" class="form-control md" placeholder="비밀번호를 입력하세요"
v-model="member.pswd" @keydown.enter="fnLogin" />
</div>
<!-- 로그인 버튼 -->
<button class="btn md main" :class="{ 'user-btn': !isAdminPage }" @click="fnLogin" @keydown.enter="fnLogin">
로그인
</button>
<!-- 사용자 페이지 전용 기능 -->
<div v-if="!isAdminPage">
<!-- 아이디/비밀번호 찾기 -->
<div class="input-group">
<p class="pl10 pr10 cursor" @click="moveSearchId">아이디찾기</p>
<p class="pl10 pr0 cursor" @click="moveResetPswd">비밀번호 초기화</p>
</div>
<!-- OAuth2 소셜 로그인 -->
<div>
<div><span>또는</span></div>
<div>
<button @click="fnOAuthLogin('kakao')" :disabled="isOAuthLoading">카카오로 로그인</button>
<button @click="fnOAuthLogin('naver')" :disabled="isOAuthLoading">네이버로 로그인</button>
<button @click="fnOAuthLogin('google')" :disabled="isOAuthLoading">구글로 로그인</button>
</div>
</div>
</div>
</div>
</div>
<!-- 2차 인증 화면 -->
<div class="login-wrap" v-else-if="loginStep1 && !loginStep2">
<div>
<p>인증코드 입력</p>
<p>{{memberInfo.email}}로 전송된 6자리 인증코드를 입력하세요.</p>
<input type="text" class="form-control md" ref="code" @input="inputCode"
v-model="memberInfo.code" maxlength="6" placeholder="인증코드를 입력하세요." />
</div>
<div class="btn-wrap">
<button class="btn sm main" @click="fnCheck">인증코드 확인</button>
</div>
<div>
<p>인증코드를 받지 못하셨나요?</p>
<button class="btn sm tertiary" @click="fnResend">인증코드 다시받기</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { useStore } from "vuex";
import { loginProc, getUserInfo, oauthLogin } from "../../../resources/api/login";
import { check2ndAuthProc, sendAuthEmailProc } from "../../../resources/api/email";
import queryParams from "../../../resources/js/queryParams";
export default {
mixins: [queryParams],
data() {
return {
isLoading: false,
member: { lgnId: null, pswd: null, lgnReqPage: 'U' },
isAdminPage: false,
isOAuthLoading: false,
memberInfo: { email: '', code: '' },
loginStep1: false, // 1차 인증
loginStep2: false, // 2차 인증
};
},
async created() {
this.checkAdminPage();
},
async mounted() {
await this.$nextTick();
if (this.hasOAuthParams()) {
await this.handleOAuthCallback();
}
},
beforeUnmount() {
this.isOAuthLoading = false;
},
watch: {
'$route'(to) {
if (to.query.oauth_success || to.query.error) {
this.handleOAuthCallback();
}
}
},
methods: {
// ========== 초기화 ==========
checkAdminPage() {
const redirect = this.restoreRedirect("redirect");
this.isAdminPage = redirect && redirect.includes("/adm/");
},
hasOAuthParams() {
return window.location.search.includes('oauth_success') ||
window.location.search.includes('error') ||
this.$route.query.oauth_success ||
this.$route.query.error;
},
// ========== 일반 로그인 ==========
async fnLogin() {
this.isLoading = true;
try {
const res = await loginProc(this.member);
if (res.status !== 200) return;
if (res.data.email) {
this.memberInfo = res.data;
} else {
this.loginStep2 = true;
await this.loginSuccessProc(res);
}
this.loginStep1 = true;
} catch (error) {
alert(error.response?.data?.message || "로그인에 실패했습니다.");
} finally {
this.isLoading = false;
}
},
async loginSuccessProc(res) {
const loginMode = res.headers['loginmode']; // J, S
const policyMode = res.headers['policymode']; // Y, N
this.$store.commit("setLoginMode", loginMode || 'J');
this.$store.commit("setPolicyMode", policyMode || 'Y');
if (loginMode === 'J') {
this.handleJWTLogin(res);
} else if (loginMode === 'S') {
this.handleSessionLogin(res);
} else {
alert("알 수 없는 로그인 방식입니다.");
return;
}
await this.handleLoginSuccess();
},
handleJWTLogin(res) {
const token = res.headers.authorization;
const userInfo = this.parseJWT(token);
this.setAuthInfo("J", token, userInfo);
},
handleSessionLogin(res) {
const userInfo = res.data;
const roles = userInfo.roles.map(r => ({ authority: r.authrtCd }));
this.setAuthInfo("S", null, { ...userInfo, roles });
},
parseJWT(token) {
const base64String = token.split(".")[1];
const base64 = base64String.replace(/-/g, "+").replace(/_/g, "/");
const jsonPayload = decodeURIComponent(
atob(base64).split("").map((c) => {
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
}).join("")
);
return JSON.parse(jsonPayload);
},
// ========== 2차 인증 ==========
inputCode(event) {
const input = event.target.value.replace(/[^0-9]/g, '');
this.memberInfo.code = input;
},
async fnCheck() {
try {
const res = await check2ndAuthProc(this.memberInfo);
if (res.status == 200) {
await this.loginSuccessProc(res);
}
} catch (error) {
const errorData = error.response.data;
if (errorData.message != null && errorData.message != "") {
alert(error.response.data.message);
this.$refs.code.focus();
} else {
alert("에러가 발생했습니다.\n관리자에게 문의해주세요.");
}
}
},
async fnResend() {
this.isLoading = true;
try {
const res = await sendAuthEmailProc(this.memberInfo);
if (res.status == 200) {
alert(res.data.message);
}
} catch (error) {
const errorData = error.response.data;
if (errorData.message != null && errorData.message != "") {
alert(error.response.data.message);
} else {
alert("에러가 발생했습니다.\n관리자에게 문의해주세요.");
}
} finally {
this.isLoading = false;
}
},
// ========== OAuth2 로그인 ==========
fnOAuthLogin(provider) {
this.isOAuthLoading = true;
const redirectUrl = this.restoreRedirect("redirect") || this.$route.fullPath;
sessionStorage.setItem('oauth_redirect', redirectUrl);
sessionStorage.setItem('oauth_provider', provider);
sessionStorage.setItem('oauth_start_time', Date.now().toString());
oauthLogin(provider);
},
async handleOAuthCallback() {
const { error, errorMessage, oauthSuccess, loginMode, policyMode } = this.parseOAuthParams();
if (error) {
this.handleOAuthError(error, errorMessage);
return;
}
if (oauthSuccess !== 'true' && oauthSuccess !== true) {
return;
}
try {
const finalLoginMode = loginMode || this.$store.state.loginMode || 'J';
const finalPolicyMode = policyMode || this.$store.state.policyMode || 'Y';
this.$store.commit("setLoginMode", finalLoginMode);
this.$store.commit("setPolicyMode", finalPolicyMode);
if (finalLoginMode === 'J') {
await this.handleOAuthJWT();
} else {
await this.handleOAuthSession();
}
this.cleanupOAuth();
await this.$nextTick();
await this.handleLoginSuccess();
} catch (error) {
this.handleOAuthError('processing_error', error.message);
}
},
parseOAuthParams() {
const urlParams = new URLSearchParams(window.location.search);
const routeQuery = this.$route.query;
return {
error: urlParams.get('error') || routeQuery.error,
errorMessage: urlParams.get('message') || routeQuery.message,
oauthSuccess: urlParams.get('oauth_success') || routeQuery.oauth_success,
loginMode: urlParams.get('loginMode') || routeQuery.loginMode, // J, S
policyMode: urlParams.get('policyMode') || routeQuery.policyMode // Y, N
};
},
async handleOAuthJWT() {
try {
// 여러 방법으로 토큰 찾기
const token = this.getCookie('Authorization')
|| this.getCookie('refresh')
|| localStorage.getItem('authorization')
|| sessionStorage.getItem('authorization');
const headers = {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
};
if (token) {
headers['Authorization'] = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
}
const response = await getUserInfo();
if (response.status === 200) {
const result = await response.json();
if (result.success || result.data) {
const userInfo = result.data;
// 토큰이 응답에 있으면 사용, 없으면 기존 토큰 사용
const finalToken = result.data.token || token;
const roles = Array.isArray(userInfo.roles) ?
userInfo.roles.map(r => ({ authority: r.authrtCd || r.authority })) :
userInfo.roles;
this.setAuthInfo("J", finalToken, { ...userInfo, roles });
} else {
throw new Error('서버에서 실패 응답');
}
} else {
throw new Error('API 호출 실패: ' + response.status);
}
} catch (error) {
throw error;
}
},
async handleOAuthSession() {
try {
const userInfoRes = await getUserInfo();
if (!userInfoRes || userInfoRes.status !== 200) {
throw new Error('세션 정보를 가져올 수 없습니다.');
}
const result = await userInfoRes.json();
const userInfo = result.data;
const roles = Array.isArray(userInfo.roles) ?
userInfo.roles.map(r => ({ authority: r.authrtCd || r.authority })) :
userInfo.roles;
this.setAuthInfo('S', null, { ...userInfo, roles });
} catch (error) {
throw error;
}
},
// ========== 공통 처리 ==========
setAuthInfo(loginMode, token, userInfo) {
// Store 설정
try {
if (typeof this.$store !== 'undefined' && this.$store.commit) {
this.$store.commit("setLoginMode", loginMode);
this.$store.commit("setAuthorization", token);
this.$store.commit("setMbrId", userInfo.mbrId);
this.$store.commit("setMbrNm", userInfo.mbrNm);
this.$store.commit("setRoles", userInfo.roles);
}
} catch (e) {
console.warn("store 설정 실패:", e);
}
},
async handleLoginSuccess() {
const isAdmin = this.$store.state.roles.some(role => role.authority === "ROLE_ADMIN");
let redirectUrl = this.restoreRedirect("redirect") || sessionStorage.getItem('oauth_redirect');
if (redirectUrl && this.shouldRedirectToMain(redirectUrl)) {
redirectUrl = this.$filters.ctxPath("/");
}
if (redirectUrl && !redirectUrl.startsWith(this.$store.state.contextPath) && this.$store.state.contextPath) {
redirectUrl = this.$filters.ctxPath(redirectUrl);
}
const targetPath = this.getValidRedirectPath(redirectUrl, isAdmin);
await this.$nextTick();
this.$router.push({ path: targetPath });
sessionStorage.removeItem("redirect");
sessionStorage.removeItem("oauth_redirect");
},
shouldRedirectToMain(url) {
return url.includes("/searchId.page") ||
url.includes("/resetPswd.page") ||
url.includes("/login.page") ||
url.includes("/cmslogin.page");
},
getValidRedirectPath(redirectUrl, isAdmin) {
if (redirectUrl && !redirectUrl.includes("login.page") && !redirectUrl.includes("cmslogin.page")) {
const routeExists = this.$router.getRoutes().some(route => route.path === redirectUrl);
if (routeExists) return redirectUrl;
}
return isAdmin ?
this.$filters.ctxPath("/adm/main.page") :
this.$filters.ctxPath("/");
},
// ========== 에러 처리 및 정리 ==========
handleOAuthError(error, errorMessage) {
this.isOAuthLoading = false;
this.cleanupOAuth();
const message = decodeURIComponent(errorMessage || error || 'OAuth 로그인에 실패했습니다.');
// 현재 페이지가 메인 페이지가 아니면 로그인 페이지로 이동
if (this.$route.path !== this.$filters.ctxPath("/")) {
this.$router.push({ path: this.$filters.ctxPath("/login.page") });
}
},
cleanupOAuth() {
sessionStorage.removeItem('oauth_redirect');
sessionStorage.removeItem('oauth_provider');
sessionStorage.removeItem('oauth_start_time');
this.isOAuthLoading = false;
// URL에서 OAuth 파라미터 제거
const cleanUrl = window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
},
// ========== 유틸리티 ==========
getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
const cookieValue = parts.pop().split(';').shift();
return cookieValue;
}
return null;
},
moveSearchId() {
this.$router.push({
path: this.$filters.ctxPath("/resetPswd.page"),
query: { tab: "id" }
});
},
moveResetPswd() {
this.$router.push({
path: this.$filters.ctxPath("/resetPswd.page"),
query: { tab: "pw" }
});
}
}
};
</script>
<style scoped>
.loading-blur {
pointer-events: none;
opacity: 0.4;
filter: grayscale(20%) blur(1px);
}
</style>