
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
04-04
File name
Commit message
Commit date
<template>
<div class="login-page page">
<div>
<div class="background-img">
<p>
어서오세요.<br />
로그인하세요.
</p>
</div>
<div class="login-wrap">
<div class="login">
<div
:class="{
'login-title': true,
'user-login': !isAdminPage,
}"
>
LOGIN
</div>
<div class="form-group">
<label for="id" class="login-label">아이디</label>
<input
type="text"
name=""
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"
name=""
id="pw"
class="form-control md"
placeholder="비밀번호를 입력하세요"
v-model="member['pswd']"
@keydown.enter="fnLogin"
/>
</div>
<button
class="btn md main user-btn"
v-if="!isAdminPage"
@click="fnLogin"
@keydown.enter="fnLogin"
>
로그인
</button>
<button
class="btn md main"
v-else
@click="fnLogin"
@keydown.enter="fnLogin"
>
로그인
</button>
<div
class="input-group"
v-if="!isAdminPage"
>
<p class="pl10 pr10 cursor" @click="moveSearchId">아이디찾기</p>
<p class="pl10 pr0 cursor" @click="moveResetPswd">비밀번호 초기화</p>
</div>
<!-- OAuth2 로그인 버튼들 -->
<div v-if="!isAdminPage">
<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>
</div>
</template>
<script>
import { useStore } from "vuex";
import store from "../AppStore";
import { loginProc, getUserInfo, oauthLogin } from "../../../resources/api/login";
import queryParams from "../../../resources/js/queryParams";
export default {
mixins: [queryParams],
data() {
return {
member: { lgnId: null, pswd: null },
store: useStore(),
isAdminPage: false,
isOAuthLoading: false,
// oauthProviders: [
// { name: 'kakao', label: '카카오로 로그인' },
// { name: 'naver', label: '네이버로 로그인' },
// { name: 'google', label: '구글로 로그인' }
// ]
};
},
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() {
try {
const res = await loginProc(this.member);
if (res.status !== 200) return;
const loginType = res.headers['login-type'];
if (loginType === 'J') {
this.handleJWTLogin(res);
} else if (loginType === 'S') {
this.handleSessionLogin(res);
} else {
alert("알 수 없는 로그인 방식입니다.");
return;
}
await this.handleLoginSuccess();
} catch (error) {
alert(error.response?.data?.message || "로그인에 실패했습니다.");
}
},
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);
},
setAuthInfo(loginMode, token, userInfo) {
// Vuex 상태 저장
store.commit("setLoginMode", loginMode);
store.commit("setAuthorization", token);
store.commit("setMbrId", userInfo.mbrId);
store.commit("setMbrNm", userInfo.mbrNm);
store.commit("setRoles", userInfo.roles);
// localStorage 저장
localStorage.setItem("loginMode", loginMode);
localStorage.setItem("mbrId", userInfo.mbrId);
localStorage.setItem("mbrNm", userInfo.mbrNm);
localStorage.setItem("roles", JSON.stringify(userInfo.roles));
if (token) {
localStorage.setItem("authorization", token);
} else {
localStorage.removeItem("authorization");
}
},
// ========== OAuth2 로그인 ==========
fnOAuthLogin(provider) {
const redirectUrl = this.restoreRedirect("redirect") || this.$route.fullPath;
sessionStorage.setItem('oauth_redirect', redirectUrl);
oauthLogin(provider);
},
async handleOAuthCallback() {
const { error, errorMessage, oauthSuccess, loginMode } = this.parseOAuthParams();
if (error) {
this.handleOAuthError(error, errorMessage);
return;
}
if (oauthSuccess !== 'true' && oauthSuccess !== true) return;
try {
store.commit("setLoginMode", loginMode || "J");
localStorage.setItem("loginMode", loginMode || "J");
if (loginMode === 'J') {
await this.handleOAuthJWT();
} else if (loginMode === 'S') {
await this.handleOAuthSession();
} else {
throw new Error('알 수 없는 로그인 모드: ' + loginMode);
}
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
};
},
async handleOAuthJWT() {
const oauthToken = this.getCookie('oauth_access_token');
if (!oauthToken) {
throw new Error('OAuth 토큰을 찾을 수 없습니다.');
}
const fullToken = oauthToken.startsWith('Bearer ') ? oauthToken : `Bearer ${oauthToken}`;
store.commit("setAuthorization", fullToken);
localStorage.setItem("authorization", fullToken);
this.deleteCookie('oauth_access_token');
try {
const userInfo = this.parseJWT(fullToken.replace('Bearer ', ''));
this.setAuthInfo("J", fullToken, userInfo);
} catch (jwtError) {
await this.fetchUserInfoFromServer();
}
},
async handleOAuthSession() {
store.commit("setAuthorization", null);
localStorage.removeItem("authorization");
this.deleteCookie('oauth_access_token');
await this.fetchUserInfoFromServer();
},
async fetchUserInfoFromServer() {
const userInfoRes = await getUserInfo();
if (!userInfoRes || userInfoRes.status !== 200) {
throw new Error('사용자 정보를 가져올 수 없습니다.');
}
const userInfo = userInfoRes.data.data;
const roles = Array.isArray(userInfo.roles) ?
userInfo.roles.map(r => ({ authority: r.authrtCd || r.authority })) :
userInfo.roles;
const loginMode = localStorage.getItem("loginMode");
const token = userInfo.token || localStorage.getItem("authorization");
this.setAuthInfo(loginMode, token, { ...userInfo, roles });
},
// ========== 로그인 성공 후 처리 ==========
async handleLoginSuccess() {
const isAdmin = store.state.roles.some(role => role.authority === "ROLE_ADMIN");
let redirectUrl = this.restoreRedirect("redirect") || sessionStorage.getItem('oauth_redirect');
// 리다이렉트 URL 정리
if (redirectUrl && this.shouldRedirectToMain(redirectUrl)) {
redirectUrl = this.$filters.ctxPath("/");
}
// Context Path 처리
if (redirectUrl && !redirectUrl.startsWith(store.state.contextPath) && 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");
},
getValidRedirectPath(redirectUrl, isAdmin) {
if (redirectUrl) {
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 로그인에 실패했습니다.');
alert(`소셜 로그인 실패: ${message}`);
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;
const cleanUrl = window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
},
// ========== 유틸리티 메서드 ==========
getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
return parts.length === 2 ? parts.pop().split(';').shift() : null;
},
deleteCookie(name) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
},
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>