외부 시퀀스 설계 완료

- 3개 핵심 비즈니스 플로우별 외부 시퀀스 다이어그램 작성
  - 사용자인증플로우.puml: UFR-AUTH-010, UFR-AUTH-020 반영
  - 요금조회플로우.puml: UFR-BILL-010~040 반영
  - 상품변경플로우.puml: UFR-PROD-010~040 반영

- 논리아키텍처와 참여자 완전 일치
- UI/UX 설계서 사용자 플로우 100% 반영
- 클라우드 패턴 적용 (API Gateway, Cache-Aside, Circuit Breaker)
- PlantUML 문법 검사 통과 (mono 테마 적용)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
hiondal
2025-09-08 10:27:39 +09:00
parent db7d66a9fc
commit 7ec8a682c6
27 changed files with 1904 additions and 0 deletions
+485
View File
@@ -0,0 +1,485 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>로그인 - 통신요금 관리 서비스</title>
<style>
/* CSS Variables - Style Guide */
:root {
/* Primary Colors */
--primary-50: #EBF8FF;
--primary-100: #BEE3F8;
--primary-200: #90CDF4;
--primary-300: #63B3ED;
--primary-400: #4299E1;
--primary-500: #3182CE;
--primary-600: #2B77CB;
--primary-700: #2C5282;
--primary-800: #2A4365;
--primary-900: #1A365D;
/* Gray Colors */
--gray-50: #F9FAFB;
--gray-100: #F3F4F6;
--gray-200: #E5E7EB;
--gray-300: #D1D5DB;
--gray-400: #9CA3AF;
--gray-500: #6B7280;
--gray-600: #4B5563;
--gray-700: #374151;
--gray-800: #1F2937;
--gray-900: #111827;
/* Status Colors */
--success-500: #38A169;
--error-500: #E53E3E;
--warning-500: #ED8936;
/* Spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
/* Typography */
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
--text-4xl: 2.25rem;
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
}
/* Reset & Base */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
font-size: var(--text-base);
line-height: 1.5;
color: var(--gray-700);
background-color: var(--gray-50);
min-height: 100vh;
}
/* Container */
.container {
width: 100%;
max-width: 400px;
margin: 0 auto;
padding: var(--space-4);
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
}
@media (min-width: 768px) {
.container {
max-width: 480px;
padding: var(--space-8);
}
}
/* Header */
.header {
text-align: center;
margin-bottom: var(--space-8);
}
.logo {
width: 80px;
height: 80px;
background: linear-gradient(135deg, var(--primary-500) 0%, var(--primary-600) 100%);
border-radius: 20px;
margin: 0 auto var(--space-4);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: var(--text-2xl);
font-weight: var(--font-bold);
}
.service-title {
font-size: var(--text-2xl);
font-weight: var(--font-bold);
color: var(--gray-900);
margin-bottom: var(--space-2);
}
.service-subtitle {
font-size: var(--text-base);
color: var(--gray-500);
}
/* Card */
.card {
background-color: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
padding: var(--space-8);
margin-bottom: var(--space-6);
}
/* Form */
.form {
display: flex;
flex-direction: column;
gap: var(--space-6);
}
.form-group {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.label {
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--gray-700);
}
.input {
width: 100%;
padding: var(--space-4);
border: 2px solid var(--gray-200);
border-radius: 12px;
font-size: var(--text-base);
line-height: 1.5;
transition: all 0.2s ease-in-out;
min-height: 52px;
background-color: white;
}
.input:focus {
outline: none;
border-color: var(--primary-500);
box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.1);
}
.input::placeholder {
color: var(--gray-400);
}
.input.error {
border-color: var(--error-500);
}
/* Checkbox */
.checkbox-group {
display: flex;
align-items: center;
gap: var(--space-3);
}
.checkbox {
width: 20px;
height: 20px;
border: 2px solid var(--gray-300);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.checkbox:checked {
background-color: var(--primary-500);
border-color: var(--primary-500);
}
.checkbox-label {
font-size: var(--text-sm);
color: var(--gray-600);
cursor: pointer;
}
/* Button */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--space-4) var(--space-6);
border-radius: 12px;
font-size: var(--text-base);
font-weight: var(--font-semibold);
line-height: 1.5;
transition: all 0.2s ease-in-out;
cursor: pointer;
border: none;
min-height: 52px;
text-decoration: none;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-500) 0%, var(--primary-600) 100%);
color: white;
box-shadow: 0 2px 8px rgba(49, 130, 206, 0.2);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(49, 130, 206, 0.3);
}
.btn-primary:disabled {
background: var(--gray-300);
color: var(--gray-500);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* Alert */
.alert {
padding: var(--space-4);
border-radius: 12px;
font-size: var(--text-sm);
margin-bottom: var(--space-4);
display: none;
}
.alert.show {
display: block;
}
.alert-error {
background-color: #FEF2F2;
border: 1px solid #FECACA;
color: #991B1B;
}
/* Footer */
.footer {
text-align: center;
margin-top: var(--space-8);
padding-top: var(--space-6);
border-top: 1px solid var(--gray-200);
}
.footer-text {
font-size: var(--text-xs);
color: var(--gray-400);
}
/* Loading */
.loading {
position: relative;
pointer-events: none;
}
.loading::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<div class="logo">📱</div>
<h1 class="service-title">통신요금 관리</h1>
<p class="service-subtitle">간편하고 안전한 요금 관리 서비스</p>
</div>
<!-- Login Form Card -->
<div class="card">
<!-- Error Alert -->
<div id="errorAlert" class="alert alert-error">
<span id="errorMessage"></span>
</div>
<form class="form" id="loginForm">
<!-- ID Input -->
<div class="form-group">
<label for="userId" class="label">아이디</label>
<input
type="text"
id="userId"
name="userId"
class="input"
placeholder="아이디를 입력하세요"
required
autocomplete="username"
aria-describedby="userId-error"
>
</div>
<!-- Password Input -->
<div class="form-group">
<label for="password" class="label">비밀번호</label>
<input
type="password"
id="password"
name="password"
class="input"
placeholder="비밀번호를 입력하세요"
required
autocomplete="current-password"
aria-describedby="password-error"
>
</div>
<!-- Auto Login Checkbox -->
<div class="checkbox-group">
<input type="checkbox" id="autoLogin" name="autoLogin" class="checkbox">
<label for="autoLogin" class="checkbox-label">자동 로그인</label>
</div>
<!-- Login Button -->
<button type="submit" class="btn btn-primary" id="loginBtn">
로그인
</button>
</form>
</div>
<!-- Footer -->
<div class="footer">
<p class="footer-text">© 2025 통신요금 관리 서비스. All rights reserved.</p>
</div>
</div>
<script>
// Login form validation and submission
const loginForm = document.getElementById('loginForm');
const loginBtn = document.getElementById('loginBtn');
const errorAlert = document.getElementById('errorAlert');
const errorMessage = document.getElementById('errorMessage');
const userIdInput = document.getElementById('userId');
const passwordInput = document.getElementById('password');
let loginAttempts = 0;
const maxAttempts = 5;
// Show error message
function showError(message) {
errorMessage.textContent = message;
errorAlert.classList.add('show');
}
// Hide error message
function hideError() {
errorAlert.classList.remove('show');
}
// Validate form inputs
function validateForm() {
const userId = userIdInput.value.trim();
const password = passwordInput.value.trim();
if (!userId) {
showError('아이디를 입력해주세요.');
userIdInput.focus();
return false;
}
if (!password) {
showError('비밀번호를 입력해주세요.');
passwordInput.focus();
return false;
}
return true;
}
// Handle form submission
loginForm.addEventListener('submit', function(e) {
e.preventDefault();
hideError();
if (!validateForm()) {
return;
}
// Check login attempts
if (loginAttempts >= maxAttempts) {
showError('로그인 시도 횟수를 초과했습니다. 30분 후 다시 시도해주세요.');
return;
}
// Show loading state
loginBtn.classList.add('loading');
loginBtn.disabled = true;
// Simulate login API call
setTimeout(() => {
const userId = userIdInput.value.trim();
const password = passwordInput.value.trim();
// Demo login - accept any ID/password for prototype
if (userId && password) {
// Success - redirect to main page
alert('로그인 성공! 메인 화면으로 이동합니다.');
window.location.href = '02-메인화면.html';
} else {
// Failure
loginAttempts++;
const remainingAttempts = maxAttempts - loginAttempts;
if (remainingAttempts > 0) {
showError(`로그인에 실패했습니다. ${remainingAttempts}회 더 시도할 수 있습니다.`);
} else {
showError('로그인 시도 횟수를 초과했습니다. 30분 후 다시 시도해주세요.');
}
loginBtn.classList.remove('loading');
loginBtn.disabled = false;
}
}, 1000);
});
// Input validation feedback
[userIdInput, passwordInput].forEach(input => {
input.addEventListener('input', function() {
this.classList.remove('error');
hideError();
});
input.addEventListener('blur', function() {
if (!this.value.trim()) {
this.classList.add('error');
}
});
});
// Enter key handling for accessibility
document.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && e.target.tagName !== 'BUTTON') {
loginForm.requestSubmit();
}
});
</script>
</body>
</html>
+537
View File
@@ -0,0 +1,537 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>메인 화면 - 통신요금 관리 서비스</title>
<style>
/* CSS Variables - Style Guide */
:root {
/* Primary Colors */
--primary-50: #EBF8FF;
--primary-100: #BEE3F8;
--primary-200: #90CDF4;
--primary-300: #63B3ED;
--primary-400: #4299E1;
--primary-500: #3182CE;
--primary-600: #2B77CB;
--primary-700: #2C5282;
--primary-800: #2A4365;
--primary-900: #1A365D;
/* Gray Colors */
--gray-50: #F9FAFB;
--gray-100: #F3F4F6;
--gray-200: #E5E7EB;
--gray-300: #D1D5DB;
--gray-400: #9CA3AF;
--gray-500: #6B7280;
--gray-600: #4B5563;
--gray-700: #374151;
--gray-800: #1F2937;
--gray-900: #111827;
/* Status Colors */
--success-50: #F0FFF4;
--success-500: #38A169;
--error-500: #E53E3E;
--warning-50: #FFFAF0;
--warning-500: #ED8936;
/* Spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
/* Typography */
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
--text-4xl: 2.25rem;
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
}
/* Reset & Base */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
font-size: var(--text-base);
line-height: 1.5;
color: var(--gray-700);
background-color: var(--gray-50);
min-height: 100vh;
}
/* Container */
.container {
width: 100%;
max-width: 480px;
margin: 0 auto;
padding: var(--space-4);
min-height: 100vh;
display: flex;
flex-direction: column;
}
@media (min-width: 768px) {
.container {
max-width: 600px;
padding: var(--space-6);
}
}
/* Header */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4) 0;
margin-bottom: var(--space-6);
}
.header-left {
display: flex;
align-items: center;
gap: var(--space-3);
}
.logo {
width: 40px;
height: 40px;
background: linear-gradient(135deg, var(--primary-500) 0%, var(--primary-600) 100%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: var(--text-lg);
font-weight: var(--font-bold);
}
.service-title {
font-size: var(--text-lg);
font-weight: var(--font-bold);
color: var(--gray-900);
}
.logout-btn {
padding: var(--space-2) var(--space-4);
background-color: white;
color: var(--gray-600);
border: 1px solid var(--gray-300);
border-radius: 8px;
font-size: var(--text-sm);
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.logout-btn:hover {
background-color: var(--gray-50);
color: var(--gray-700);
}
/* Welcome Section */
.welcome-section {
background: linear-gradient(135deg, var(--primary-500) 0%, var(--primary-600) 100%);
color: white;
padding: var(--space-8);
border-radius: 20px;
margin-bottom: var(--space-8);
box-shadow: 0 4px 20px rgba(49, 130, 206, 0.2);
}
.welcome-title {
font-size: var(--text-2xl);
font-weight: var(--font-bold);
margin-bottom: var(--space-2);
}
.user-info {
font-size: var(--text-base);
opacity: 0.9;
margin-bottom: var(--space-4);
}
.user-details {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
@media (min-width: 768px) {
.user-details {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
}
.user-phone {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
background-color: rgba(255, 255, 255, 0.2);
padding: var(--space-2) var(--space-4);
border-radius: 999px;
display: inline-block;
}
.current-product {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-1);
}
@media (min-width: 768px) {
.current-product {
align-items: flex-end;
text-align: right;
}
}
.current-product-label {
font-size: var(--text-sm);
opacity: 0.8;
font-weight: var(--font-normal);
}
.current-product-name {
font-size: var(--text-base);
font-weight: var(--font-semibold);
background-color: rgba(255, 255, 255, 0.2);
padding: var(--space-1) var(--space-3);
border-radius: 8px;
display: inline-block;
}
/* Service Menu */
.service-menu {
flex: 1;
}
.menu-title {
font-size: var(--text-xl);
font-weight: var(--font-bold);
color: var(--gray-900);
margin-bottom: var(--space-6);
}
.menu-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-4);
}
@media (min-width: 768px) {
.menu-grid {
grid-template-columns: repeat(2, 1fr);
gap: var(--space-6);
}
}
/* Menu Card */
.menu-card {
background-color: white;
border-radius: 16px;
padding: var(--space-6);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid var(--gray-100);
transition: all 0.3s ease;
cursor: pointer;
text-decoration: none;
color: inherit;
display: block;
}
.menu-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
}
.menu-card.disabled {
opacity: 0.5;
cursor: not-allowed;
background-color: var(--gray-50);
}
.menu-card.disabled:hover {
transform: none;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.menu-icon {
width: 60px;
height: 60px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-2xl);
margin-bottom: var(--space-4);
}
.menu-icon.bill {
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
}
.menu-icon.product {
background: linear-gradient(135deg, #F59E0B 0%, #D97706 100%);
}
.menu-title-text {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--gray-900);
margin-bottom: var(--space-2);
}
.menu-description {
font-size: var(--text-sm);
color: var(--gray-500);
line-height: 1.6;
}
/* Access Denied Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background-color: white;
border-radius: 16px;
padding: var(--space-8);
margin: var(--space-4);
max-width: 320px;
width: 100%;
text-align: center;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
}
.modal-icon {
width: 60px;
height: 60px;
background-color: var(--error-500);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto var(--space-4);
color: white;
font-size: var(--text-2xl);
}
.modal-title {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--gray-900);
margin-bottom: var(--space-2);
}
.modal-message {
font-size: var(--text-sm);
color: var(--gray-600);
margin-bottom: var(--space-6);
line-height: 1.6;
}
.modal-btn {
padding: var(--space-3) var(--space-6);
background-color: var(--primary-500);
color: white;
border: none;
border-radius: 8px;
font-size: var(--text-sm);
font-weight: var(--font-medium);
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.modal-btn:hover {
background-color: var(--primary-600);
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<div class="header-left">
<div class="logo">📱</div>
<h1 class="service-title">통신요금 관리</h1>
</div>
<button class="logout-btn" onclick="handleLogout()">로그아웃</button>
</div>
<!-- Welcome Section -->
<div class="welcome-section">
<h2 class="welcome-title">안녕하세요!</h2>
<p class="user-info">통신요금 관리 서비스에 오신 것을 환영합니다.</p>
<div class="user-details">
<span class="user-phone">010-1234-5678</span>
<div class="current-product">
<span class="current-product-label">현재 상품</span>
<span class="current-product-name" id="currentProductName">5G 프리미엄 플랜</span>
</div>
</div>
</div>
<!-- Service Menu -->
<div class="service-menu">
<h3 class="menu-title">서비스 메뉴</h3>
<div class="menu-grid">
<!-- 요금 조회 메뉴 -->
<a href="03-요금조회메뉴.html" class="menu-card" id="billCard">
<div class="menu-icon bill">📊</div>
<h4 class="menu-title-text">요금 조회</h4>
<p class="menu-description">
월별 통신요금과 사용량을<br>
상세하게 확인할 수 있습니다.
</p>
</a>
<!-- 상품 변경 메뉴 -->
<a href="05-상품변경메뉴.html" class="menu-card" id="productCard">
<div class="menu-icon product">🔄</div>
<h4 class="menu-title-text">상품 변경</h4>
<p class="menu-description">
현재 이용 중인 요금제를<br>
다른 상품으로 변경할 수 있습니다.
</p>
</a>
</div>
</div>
</div>
<!-- Access Denied Modal -->
<div class="modal-overlay" id="accessDeniedModal">
<div class="modal">
<div class="modal-icon">🚫</div>
<h3 class="modal-title">접근 권한 없음</h3>
<p class="modal-message">해당 서비스를 이용할 권한이 없습니다.<br>고객센터로 문의해주세요.</p>
<button class="modal-btn" onclick="closeModal()">확인</button>
</div>
</div>
<script>
// User permissions simulation
const userPermissions = {
bill: true, // 요금 조회 권한
product: true // 상품 변경 권한
};
// Initialize page
document.addEventListener('DOMContentLoaded', function() {
loadCurrentProduct();
checkPermissions();
});
// Load current product from localStorage
function loadCurrentProduct() {
try {
const currentProduct = localStorage.getItem('currentProduct');
if (currentProduct) {
const product = JSON.parse(currentProduct);
const productNameElement = document.getElementById('currentProductName');
if (productNameElement && product.name) {
productNameElement.textContent = product.name;
}
}
} catch (error) {
console.warn('localStorage에서 상품 정보를 불러오는 중 오류 발생:', error);
// 기본값 유지 (5G 프리미엄 플랜)
}
}
// Check user permissions and update UI
function checkPermissions() {
const billCard = document.getElementById('billCard');
const productCard = document.getElementById('productCard');
// 요금 조회 권한 확인
if (!userPermissions.bill) {
billCard.classList.add('disabled');
billCard.href = '#';
billCard.addEventListener('click', function(e) {
e.preventDefault();
showAccessDeniedModal('요금 조회');
});
}
// 상품 변경 권한 확인
if (!userPermissions.product) {
productCard.classList.add('disabled');
productCard.href = '#';
productCard.addEventListener('click', function(e) {
e.preventDefault();
showAccessDeniedModal('상품 변경');
});
}
}
// Show access denied modal
function showAccessDeniedModal(serviceName) {
const modal = document.getElementById('accessDeniedModal');
const message = modal.querySelector('.modal-message');
message.innerHTML = `${serviceName} 서비스를 이용할 권한이 없습니다.<br>고객센터로 문의해주세요.`;
modal.style.display = 'flex';
}
// Close modal
function closeModal() {
const modal = document.getElementById('accessDeniedModal');
modal.style.display = 'none';
}
// Handle logout
function handleLogout() {
if (confirm('로그아웃 하시겠습니까?')) {
alert('안전하게 로그아웃되었습니다.');
window.location.href = '01-로그인.html';
}
}
// Modal close on overlay click
document.getElementById('accessDeniedModal').addEventListener('click', function(e) {
if (e.target === this) {
closeModal();
}
});
// Escape key to close modal
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeModal();
}
});
</script>
</body>
</html>
@@ -0,0 +1,690 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>요금조회 메뉴 - 통신요금 관리 서비스</title>
<style>
/* CSS Variables - Style Guide */
:root {
/* Primary Colors */
--primary-50: #EBF8FF;
--primary-100: #BEE3F8;
--primary-200: #90CDF4;
--primary-300: #63B3ED;
--primary-400: #4299E1;
--primary-500: #3182CE;
--primary-600: #2B77CB;
--primary-700: #2C5282;
--primary-800: #2A4365;
--primary-900: #1A365D;
/* Gray Colors */
--gray-50: #F9FAFB;
--gray-100: #F3F4F6;
--gray-200: #E5E7EB;
--gray-300: #D1D5DB;
--gray-400: #9CA3AF;
--gray-500: #6B7280;
--gray-600: #4B5563;
--gray-700: #374151;
--gray-800: #1F2937;
--gray-900: #111827;
/* Status Colors */
--success-50: #F0FFF4;
--success-500: #38A169;
--error-500: #E53E3E;
--warning-50: #FFFAF0;
--warning-500: #ED8936;
/* Spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
/* Typography */
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
--text-4xl: 2.25rem;
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
}
/* Reset & Base */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
font-size: var(--text-base);
line-height: 1.5;
color: var(--gray-700);
background-color: var(--gray-50);
min-height: 100vh;
}
/* Container */
.container {
width: 100%;
max-width: 480px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: white;
}
@media (min-width: 768px) {
.container {
max-width: 600px;
}
}
/* Header */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4);
background-color: white;
border-bottom: 1px solid var(--gray-200);
position: sticky;
top: 0;
z-index: 100;
}
.header-left {
display: flex;
align-items: center;
gap: var(--space-3);
}
.back-btn {
width: 40px;
height: 40px;
border: none;
background-color: var(--gray-100);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease-in-out;
color: var(--gray-700);
font-size: var(--text-lg);
}
.back-btn:hover {
background-color: var(--gray-200);
}
.page-title {
font-size: var(--text-xl);
font-weight: var(--font-semibold);
color: var(--gray-900);
}
.menu-btn {
width: 40px;
height: 40px;
border: none;
background: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--gray-600);
font-size: var(--text-lg);
}
/* Main Content */
.main-content {
flex: 1;
padding: var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-8);
}
/* User Info Card */
.user-info-card {
background: linear-gradient(135deg, var(--primary-500) 0%, var(--primary-600) 100%);
color: white;
padding: var(--space-6);
border-radius: 16px;
box-shadow: 0 4px 20px rgba(49, 130, 206, 0.2);
}
.user-info-title {
font-size: var(--text-sm);
opacity: 0.9;
margin-bottom: var(--space-2);
}
.user-phone {
font-size: var(--text-2xl);
font-weight: var(--font-bold);
display: flex;
align-items: center;
gap: var(--space-3);
}
/* Inquiry Options Card */
.inquiry-card {
background-color: white;
border-radius: 16px;
padding: var(--space-6);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid var(--gray-100);
}
.card-title {
font-size: var(--text-xl);
font-weight: var(--font-semibold);
color: var(--gray-900);
margin-bottom: var(--space-6);
display: flex;
align-items: center;
gap: var(--space-3);
}
.card-title-icon {
width: 24px;
height: 24px;
background-color: var(--primary-100);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: var(--primary-600);
font-size: var(--text-sm);
}
.form-group {
margin-bottom: var(--space-6);
}
.form-label {
display: block;
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--gray-700);
margin-bottom: var(--space-3);
}
.select-wrapper {
position: relative;
}
.select {
width: 100%;
padding: var(--space-4);
border: 2px solid var(--gray-200);
border-radius: 12px;
font-size: var(--text-base);
line-height: 1.5;
background-color: white;
cursor: pointer;
appearance: none;
transition: all 0.2s ease-in-out;
min-height: 52px;
}
.select:focus {
outline: none;
border-color: var(--primary-500);
box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.1);
}
.select-wrapper::after {
content: "▼";
position: absolute;
top: 50%;
right: var(--space-4);
transform: translateY(-50%);
color: var(--gray-400);
font-size: var(--text-sm);
pointer-events: none;
}
.form-help {
font-size: var(--text-sm);
color: var(--gray-500);
margin-top: var(--space-2);
display: flex;
align-items: center;
gap: var(--space-2);
}
.help-icon {
color: var(--primary-500);
font-size: var(--text-xs);
}
/* Action Buttons */
.action-buttons {
padding: var(--space-6);
padding-top: 0;
display: flex;
gap: var(--space-4);
}
.btn {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--space-4) var(--space-6);
border-radius: 12px;
font-size: var(--text-base);
font-weight: var(--font-semibold);
line-height: 1.5;
transition: all 0.2s ease-in-out;
cursor: pointer;
border: none;
min-height: 52px;
text-decoration: none;
position: relative;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-500) 0%, var(--primary-600) 100%);
color: white;
box-shadow: 0 2px 8px rgba(49, 130, 206, 0.2);
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(49, 130, 206, 0.3);
}
.btn-secondary {
background-color: white;
color: var(--gray-700);
border: 2px solid var(--gray-300);
}
.btn-secondary:hover {
background-color: var(--gray-50);
border-color: var(--gray-400);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* Loading State */
.btn.loading {
pointer-events: none;
}
.btn.loading::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
/* Alert */
.alert {
padding: var(--space-4);
border-radius: 12px;
font-size: var(--text-sm);
margin-bottom: var(--space-4);
display: none;
align-items: flex-start;
gap: var(--space-3);
}
.alert.show {
display: flex;
}
.alert-error {
background-color: #FEF2F2;
border: 1px solid #FECACA;
color: #991B1B;
}
.alert-warning {
background-color: var(--warning-50);
border: 1px solid #FED7AA;
color: #92400E;
}
.alert-icon {
margin-top: var(--space-1);
font-size: var(--text-base);
}
.alert-content {
flex: 1;
}
/* Usage Info */
.usage-info {
background-color: var(--gray-50);
border-radius: 12px;
padding: var(--space-4);
border: 1px solid var(--gray-200);
}
.usage-title {
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--gray-700);
margin-bottom: var(--space-2);
display: flex;
align-items: center;
gap: var(--space-2);
}
.usage-list {
list-style: none;
font-size: var(--text-sm);
color: var(--gray-600);
line-height: 1.6;
}
.usage-list li {
position: relative;
padding-left: var(--space-4);
margin-bottom: var(--space-1);
}
.usage-list li::before {
content: "•";
position: absolute;
left: 0;
color: var(--primary-500);
font-weight: var(--font-bold);
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<div class="header-left">
<button class="back-btn" onclick="goBack()" aria-label="뒤로가기">
</button>
<h1 class="page-title">요금 조회</h1>
</div>
<button class="menu-btn" onclick="showMenu()" aria-label="메뉴">
</button>
</div>
<!-- Main Content -->
<div class="main-content">
<!-- User Info Card -->
<div class="user-info-card">
<div class="user-info-title">조회 대상 회선</div>
<div class="user-phone">
📱 010-1234-5678
</div>
</div>
<!-- Inquiry Options Card -->
<div class="inquiry-card">
<h2 class="card-title">
<span class="card-title-icon">📅</span>
조회 옵션 설정
</h2>
<!-- Error Alert -->
<div id="errorAlert" class="alert alert-error">
<span class="alert-icon">⚠️</span>
<div class="alert-content">
<span id="errorMessage"></span>
</div>
</div>
<!-- Warning Alert -->
<div id="warningAlert" class="alert alert-warning">
<span class="alert-icon">💡</span>
<div class="alert-content">
<span id="warningMessage"></span>
</div>
</div>
<form id="inquiryForm">
<!-- 조회월 선택 -->
<div class="form-group">
<label for="inquiryMonth" class="form-label">조회월 선택</label>
<div class="select-wrapper">
<select id="inquiryMonth" name="inquiryMonth" class="select" required>
<option value="">조회할 월을 선택해주세요</option>
</select>
</div>
<div class="form-help">
<span class="help-icon"></span>
최근 6개월 요금 정보를 조회할 수 있습니다
</div>
</div>
<!-- Usage Info -->
<div class="usage-info">
<div class="usage-title">
<span>📋</span>
조회 가능한 정보
</div>
<ul class="usage-list">
<li>월 요금 상세 내역 (기본료, 통화료, 데이터료 등)</li>
<li>사용량 정보 (통화시간, 데이터 사용량, SMS 등)</li>
<li>할인 및 혜택 내역</li>
<li>단말기 할부금 및 기타 부대비용</li>
<li>약정 정보 및 예상 해지비용</li>
</ul>
</div>
</form>
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<button type="button" class="btn btn-secondary" onclick="goBack()">
취소
</button>
<button type="submit" form="inquiryForm" class="btn btn-primary" id="inquiryBtn">
요금 조회
</button>
</div>
</div>
<script>
// 현재 날짜 기준으로 최근 6개월 옵션 생성
function generateMonthOptions() {
const select = document.getElementById('inquiryMonth');
const now = new Date();
// 현재 월부터 6개월 전까지 옵션 생성
for (let i = 0; i < 6; i++) {
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const monthStr = month.toString().padStart(2, '0');
const option = document.createElement('option');
option.value = `${year}${monthStr}`;
if (i === 0) {
option.textContent = `${year}${month}월 (현재 월)`;
option.selected = true; // 기본값으로 현재 월 선택
} else {
option.textContent = `${year}${month}`;
}
select.appendChild(option);
}
}
// 폼 요소 참조
const inquiryForm = document.getElementById('inquiryForm');
const inquiryBtn = document.getElementById('inquiryBtn');
const errorAlert = document.getElementById('errorAlert');
const warningAlert = document.getElementById('warningAlert');
const errorMessage = document.getElementById('errorMessage');
const warningMessage = document.getElementById('warningMessage');
const monthSelect = document.getElementById('inquiryMonth');
// 에러 메시지 표시
function showError(message) {
errorMessage.textContent = message;
errorAlert.classList.add('show');
hideWarning();
}
// 경고 메시지 표시
function showWarning(message) {
warningMessage.textContent = message;
warningAlert.classList.add('show');
hideError();
}
// 에러 메시지 숨기기
function hideError() {
errorAlert.classList.remove('show');
}
// 경고 메시지 숨기기
function hideWarning() {
warningAlert.classList.remove('show');
}
// 메시지 전체 숨기기
function hideAllMessages() {
hideError();
hideWarning();
}
// 폼 유효성 검사
function validateForm() {
const selectedMonth = monthSelect.value;
if (!selectedMonth) {
showError('조회할 월을 선택해주세요.');
monthSelect.focus();
return false;
}
return true;
}
// 폼 제출 처리
inquiryForm.addEventListener('submit', function(e) {
e.preventDefault();
hideAllMessages();
if (!validateForm()) {
return;
}
// 로딩 상태 시작
inquiryBtn.classList.add('loading');
inquiryBtn.disabled = true;
const selectedMonth = monthSelect.value;
const selectedText = monthSelect.options[monthSelect.selectedIndex].text;
// 로딩 시뮬레이션
setTimeout(() => {
try {
// 현재 날짜와 비교하여 미래 월인지 확인
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1;
const currentYearMonth = parseInt(`${currentYear}${currentMonth.toString().padStart(2, '0')}`);
const selectedYearMonth = parseInt(selectedMonth);
if (selectedYearMonth > currentYearMonth) {
showWarning('미래 월의 요금 정보는 조회할 수 없습니다.');
inquiryBtn.classList.remove('loading');
inquiryBtn.disabled = false;
return;
}
// 성공 - 조회 결과 페이지로 이동
sessionStorage.setItem('selectedMonth', selectedMonth);
sessionStorage.setItem('selectedMonthText', selectedText);
window.location.href = '04-요금조회결과.html';
} catch (error) {
// 오류 처리
showError('요금 조회 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.');
inquiryBtn.classList.remove('loading');
inquiryBtn.disabled = false;
}
}, 1500); // 1.5초 로딩 시뮬레이션
});
// 선택 변경 시 메시지 숨기기
monthSelect.addEventListener('change', function() {
hideAllMessages();
});
// 뒤로가기
function goBack() {
if (confirm('요금 조회를 취소하고 메인 화면으로 돌아가시겠습니까?')) {
window.location.href = '02-메인화면.html';
}
}
// 메뉴 표시 (추후 구현)
function showMenu() {
alert('메뉴 기능은 추후 구현 예정입니다.');
}
// 키보드 접근성
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
hideAllMessages();
}
});
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', function() {
generateMonthOptions();
// 페이지 진입 시 현재 월 선택에 대한 안내 표시
setTimeout(() => {
showWarning('기본적으로 현재 월이 선택되어 있습니다. 다른 월을 조회하려면 드롭다운에서 선택해주세요.');
}, 500);
});
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,592 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>상품 변경 - 통신요금 관리 서비스</title>
<style>
/* CSS Variables - Style Guide */
:root {
/* Primary Colors */
--primary-50: #EBF8FF;
--primary-100: #BEE3F8;
--primary-200: #90CDF4;
--primary-300: #63B3ED;
--primary-400: #4299E1;
--primary-500: #3182CE;
--primary-600: #2B77CB;
--primary-700: #2C5282;
--primary-800: #2A4365;
--primary-900: #1A365D;
/* Gray Colors */
--gray-50: #F9FAFB;
--gray-100: #F3F4F6;
--gray-200: #E5E7EB;
--gray-300: #D1D5DB;
--gray-400: #9CA3AF;
--gray-500: #6B7280;
--gray-600: #4B5563;
--gray-700: #374151;
--gray-800: #1F2937;
--gray-900: #111827;
/* Status Colors */
--success-500: #38A169;
--error-500: #E53E3E;
--warning-500: #ED8936;
/* Spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
/* Typography */
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
--text-4xl: 2.25rem;
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
}
/* Reset & Base */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
font-size: var(--text-base);
line-height: 1.5;
color: var(--gray-700);
background-color: var(--gray-50);
min-height: 100vh;
}
/* Container */
.container {
width: 100%;
max-width: 400px;
margin: 0 auto;
padding: var(--space-4);
min-height: 100vh;
background-color: white;
}
@media (min-width: 768px) {
.container {
max-width: 480px;
padding: var(--space-6);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
}
/* Header */
.header {
position: sticky;
top: 0;
background: white;
z-index: 10;
padding: var(--space-4) 0;
border-bottom: 1px solid var(--gray-200);
margin-bottom: var(--space-6);
}
.header-content {
display: flex;
align-items: center;
gap: var(--space-4);
}
.back-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: none;
background: none;
border-radius: 8px;
cursor: pointer;
font-size: var(--text-xl);
color: var(--gray-600);
transition: all 0.2s ease-in-out;
}
.back-btn:hover {
background-color: var(--gray-100);
color: var(--gray-800);
}
.page-title {
font-size: var(--text-xl);
font-weight: var(--font-bold);
color: var(--gray-900);
}
/* Card */
.card {
background-color: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
padding: var(--space-6);
margin-bottom: var(--space-6);
border: 1px solid var(--gray-200);
}
.card-title {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--gray-900);
margin-bottom: var(--space-4);
}
/* Customer Info */
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3) 0;
border-bottom: 1px solid var(--gray-100);
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-size: var(--text-sm);
color: var(--gray-500);
font-weight: var(--font-medium);
}
.info-value {
font-size: var(--text-base);
color: var(--gray-900);
font-weight: var(--font-semibold);
}
/* Product Info */
.product-card {
background: linear-gradient(135deg, var(--primary-50) 0%, var(--primary-100) 100%);
border: 2px solid var(--primary-200);
border-radius: 16px;
padding: var(--space-6);
margin-bottom: var(--space-6);
}
.product-header {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-4);
}
.product-icon {
width: 40px;
height: 40px;
background: var(--primary-500);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: var(--text-lg);
}
.product-name {
font-size: var(--text-xl);
font-weight: var(--font-bold);
color: var(--primary-800);
}
.product-price {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--primary-700);
margin-bottom: var(--space-4);
}
.benefits-list {
list-style: none;
padding: 0;
}
.benefit-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-2) 0;
font-size: var(--text-sm);
color: var(--gray-700);
}
.benefit-icon {
width: 16px;
height: 16px;
background: var(--success-500);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 10px;
}
/* Notice */
.notice {
background: #FFF7ED;
border: 1px solid #FED7AA;
border-radius: 12px;
padding: var(--space-4);
margin-bottom: var(--space-6);
}
.notice-title {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-sm);
font-weight: var(--font-semibold);
color: var(--warning-500);
margin-bottom: var(--space-2);
}
.notice-text {
font-size: var(--text-sm);
color: #92400E;
line-height: 1.4;
}
/* Button */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--space-4) var(--space-6);
border-radius: 12px;
font-size: var(--text-base);
font-weight: var(--font-semibold);
line-height: 1.5;
transition: all 0.2s ease-in-out;
cursor: pointer;
border: none;
min-height: 52px;
text-decoration: none;
width: 100%;
margin-bottom: var(--space-3);
}
.btn:last-child {
margin-bottom: 0;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-500) 0%, var(--primary-600) 100%);
color: white;
box-shadow: 0 2px 8px rgba(49, 130, 206, 0.2);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(49, 130, 206, 0.3);
}
.btn-secondary {
background: white;
color: var(--gray-600);
border: 2px solid var(--gray-200);
}
.btn-secondary:hover {
background: var(--gray-50);
border-color: var(--gray-300);
color: var(--gray-700);
}
/* Loading */
.loading {
position: relative;
pointer-events: none;
}
.loading::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
/* Skeleton */
.skeleton {
background: linear-gradient(90deg, var(--gray-200) 25%, var(--gray-100) 50%, var(--gray-200) 75%);
background-size: 200% 100%;
animation: skeleton 1.5s infinite;
}
@keyframes skeleton {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Alert */
.alert {
padding: var(--space-4);
border-radius: 12px;
font-size: var(--text-sm);
margin-bottom: var(--space-4);
display: none;
}
.alert.show {
display: block;
}
.alert-error {
background-color: #FEF2F2;
border: 1px solid #FECACA;
color: #991B1B;
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<div class="header-content">
<button class="back-btn" onclick="goBack()" aria-label="뒤로가기">
</button>
<h1 class="page-title">상품 변경</h1>
</div>
</header>
<!-- Error Alert -->
<div id="errorAlert" class="alert alert-error">
<span id="errorMessage"></span>
</div>
<!-- Customer Info Card -->
<div class="card">
<h2 class="card-title">고객 정보</h2>
<div class="info-row">
<span class="info-label">회선번호</span>
<span class="info-value">010-1234-5678</span>
</div>
<div class="info-row">
<span class="info-label">고객ID</span>
<span class="info-value">customer123</span>
</div>
</div>
<!-- Current Product Info -->
<div class="product-card">
<div class="product-header">
<div class="product-icon">📱</div>
<div>
<div class="product-name">5G 프리미엄 플랜</div>
</div>
</div>
<div class="product-price">월 69,000원</div>
<ul class="benefits-list">
<li class="benefit-item">
<span class="benefit-icon"></span>
<span>5G 데이터 무제한</span>
</li>
<li class="benefit-item">
<span class="benefit-icon"></span>
<span>음성통화 무제한</span>
</li>
<li class="benefit-item">
<span class="benefit-icon"></span>
<span>문자 무제한</span>
</li>
<li class="benefit-item">
<span class="benefit-icon"></span>
<span>해외 로밍 50% 할인</span>
</li>
</ul>
</div>
<!-- Notice -->
<div class="notice">
<div class="notice-title">
<span>⚠️</span>
<span>상품 변경 시 주의사항</span>
</div>
<p class="notice-text">
• 상품 변경은 다음 월 1일부터 적용됩니다<br>
• 기존 약정 조건에 따라 위약금이 발생할 수 있습니다<br>
• 변경 후에는 이전 상품으로 즉시 되돌릴 수 없습니다<br>
• 부가서비스는 별도로 재신청이 필요할 수 있습니다
</p>
</div>
<!-- Action Buttons -->
<div class="actions">
<button class="btn btn-primary" id="changeBtn" onclick="goToProductChange()">
상품 변경하기
</button>
<button class="btn btn-secondary" onclick="goBack()">
취소
</button>
</div>
</div>
<script>
// Navigation functions
function goBack() {
window.history.back();
}
function goToProductChange() {
const changeBtn = document.getElementById('changeBtn');
// Show loading state
changeBtn.classList.add('loading');
changeBtn.disabled = true;
// Simulate loading
setTimeout(() => {
window.location.href = '06-상품변경화면.html';
}, 800);
}
// Show error message
function showError(message) {
const errorAlert = document.getElementById('errorAlert');
const errorMessage = document.getElementById('errorMessage');
errorMessage.textContent = message;
errorAlert.classList.add('show');
}
// Hide error message
function hideError() {
const errorAlert = document.getElementById('errorAlert');
errorAlert.classList.remove('show');
}
// Load customer and product info on page load
document.addEventListener('DOMContentLoaded', function() {
loadCurrentProduct();
// Handle keyboard navigation
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
goBack();
}
if (e.key === 'Enter' && e.target.tagName === 'BUTTON') {
e.target.click();
}
});
});
// Load current product from localStorage
function loadCurrentProduct() {
try {
const currentProduct = localStorage.getItem('currentProduct');
if (currentProduct) {
const product = JSON.parse(currentProduct);
updateCurrentProductDisplay(product);
}
} catch (error) {
console.warn('localStorage에서 상품 정보를 불러오는 중 오류 발생:', error);
// 기본값으로 화면이 이미 설정되어 있음
}
}
// Update current product display
function updateCurrentProductDisplay(product) {
// Update product name
const productNameElement = document.querySelector('.product-name');
if (productNameElement && product.name) {
productNameElement.textContent = product.name;
}
// Update product price
const productPriceElement = document.querySelector('.product-price');
if (productPriceElement && product.price) {
productPriceElement.textContent = product.price;
}
// Update benefits
if (product.benefits && Array.isArray(product.benefits)) {
const benefitsList = document.querySelector('.benefits-list');
if (benefitsList) {
benefitsList.innerHTML = '';
product.benefits.forEach(benefit => {
const li = document.createElement('li');
li.className = 'benefit-item';
li.innerHTML = `
<span class="benefit-icon">✓</span>
<span>${benefit}</span>
`;
benefitsList.appendChild(li);
});
}
}
}
// Handle back button for accessibility
window.addEventListener('popstate', function(e) {
// Handle browser back button
});
// Add focus management for accessibility
const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
const modal = document.querySelector('.container');
const firstFocusableElement = modal.querySelectorAll(focusableElements)[0];
const focusableContent = modal.querySelectorAll(focusableElements);
const lastFocusableElement = focusableContent[focusableContent.length - 1];
document.addEventListener('keydown', function(e) {
const isTabPressed = e.key === 'Tab';
if (!isTabPressed) {
return;
}
if (e.shiftKey) {
if (document.activeElement === firstFocusableElement) {
lastFocusableElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastFocusableElement) {
firstFocusableElement.focus();
e.preventDefault();
}
}
});
</script>
</body>
</html>
@@ -0,0 +1,797 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>상품 선택 - 통신요금 관리 서비스</title>
<style>
/* CSS Variables - Style Guide */
:root {
/* Primary Colors */
--primary-50: #EBF8FF;
--primary-100: #BEE3F8;
--primary-200: #90CDF4;
--primary-300: #63B3ED;
--primary-400: #4299E1;
--primary-500: #3182CE;
--primary-600: #2B77CB;
--primary-700: #2C5282;
--primary-800: #2A4365;
--primary-900: #1A365D;
/* Gray Colors */
--gray-50: #F9FAFB;
--gray-100: #F3F4F6;
--gray-200: #E5E7EB;
--gray-300: #D1D5DB;
--gray-400: #9CA3AF;
--gray-500: #6B7280;
--gray-600: #4B5563;
--gray-700: #374151;
--gray-800: #1F2937;
--gray-900: #111827;
/* Status Colors */
--success-500: #38A169;
--error-500: #E53E3E;
--warning-500: #ED8936;
/* Spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
/* Typography */
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
--text-4xl: 2.25rem;
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
}
/* Reset & Base */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
font-size: var(--text-base);
line-height: 1.5;
color: var(--gray-700);
background-color: var(--gray-50);
min-height: 100vh;
padding-bottom: 100px; /* Space for fixed buttons */
}
/* Container */
.container {
width: 100%;
max-width: 400px;
margin: 0 auto;
padding: var(--space-4);
background-color: white;
min-height: 100vh;
}
@media (min-width: 768px) {
.container {
max-width: 480px;
padding: var(--space-6);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
}
/* Header */
.header {
position: sticky;
top: 0;
background: white;
z-index: 10;
padding: var(--space-4) 0;
border-bottom: 1px solid var(--gray-200);
margin-bottom: var(--space-6);
}
.header-content {
display: flex;
align-items: center;
gap: var(--space-4);
}
.back-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: none;
background: none;
border-radius: 8px;
cursor: pointer;
font-size: var(--text-xl);
color: var(--gray-600);
transition: all 0.2s ease-in-out;
}
.back-btn:hover {
background-color: var(--gray-100);
color: var(--gray-800);
}
.page-title {
font-size: var(--text-xl);
font-weight: var(--font-bold);
color: var(--gray-900);
}
/* Current Product Summary */
.current-product {
background: linear-gradient(135deg, var(--gray-100) 0%, var(--gray-50) 100%);
border: 2px solid var(--gray-200);
border-radius: 12px;
padding: var(--space-4);
margin-bottom: var(--space-6);
}
.current-label {
font-size: var(--text-sm);
color: var(--gray-500);
margin-bottom: var(--space-2);
font-weight: var(--font-medium);
}
.current-name {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--gray-800);
margin-bottom: var(--space-1);
}
.current-price {
font-size: var(--text-base);
color: var(--gray-600);
}
/* Product List */
.products-section {
margin-bottom: var(--space-8);
}
.section-title {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--gray-900);
margin-bottom: var(--space-4);
}
.product-card {
border: 2px solid var(--gray-200);
border-radius: 16px;
padding: var(--space-5);
margin-bottom: var(--space-4);
cursor: pointer;
transition: all 0.2s ease-in-out;
position: relative;
}
.product-card:hover {
border-color: var(--primary-300);
box-shadow: 0 4px 12px rgba(49, 130, 206, 0.1);
}
.product-card.selected {
border-color: var(--primary-500);
background: linear-gradient(135deg, var(--primary-50) 0%, var(--primary-100) 100%);
box-shadow: 0 4px 12px rgba(49, 130, 206, 0.2);
}
.product-radio {
position: absolute;
top: var(--space-4);
right: var(--space-4);
width: 20px;
height: 20px;
border: 2px solid var(--gray-300);
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.product-card.selected .product-radio {
border-color: var(--primary-500);
background: var(--primary-500);
}
.product-card.selected .product-radio::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 8px;
height: 8px;
background: white;
border-radius: 50%;
}
.product-header {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-3);
margin-right: var(--space-8);
}
.product-icon {
width: 36px;
height: 36px;
background: var(--primary-500);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: var(--text-base);
flex-shrink: 0;
}
.product-card.selected .product-icon {
background: var(--primary-600);
}
.product-info {
flex: 1;
}
.product-name {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--gray-900);
margin-bottom: var(--space-1);
}
.product-price {
font-size: var(--text-base);
font-weight: var(--font-medium);
color: var(--primary-600);
}
.benefits-grid {
display: grid;
gap: var(--space-2);
margin-bottom: var(--space-3);
}
.benefit-item {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-sm);
color: var(--gray-600);
}
.benefit-icon {
width: 14px;
height: 14px;
background: var(--success-500);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 8px;
flex-shrink: 0;
}
.price-comparison {
display: flex;
align-items: center;
gap: var(--space-3);
padding-top: var(--space-3);
border-top: 1px solid var(--gray-200);
font-size: var(--text-sm);
}
.price-change {
font-weight: var(--font-semibold);
}
.price-up {
color: var(--error-500);
}
.price-down {
color: var(--success-500);
}
.price-same {
color: var(--gray-500);
}
/* Fixed Action Buttons */
.fixed-actions {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 400px;
background: white;
padding: var(--space-4);
border-top: 1px solid var(--gray-200);
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
}
@media (min-width: 768px) {
.fixed-actions {
max-width: 480px;
padding: var(--space-6);
}
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--space-4) var(--space-6);
border-radius: 12px;
font-size: var(--text-base);
font-weight: var(--font-semibold);
line-height: 1.5;
transition: all 0.2s ease-in-out;
cursor: pointer;
border: none;
min-height: 52px;
text-decoration: none;
width: 100%;
margin-bottom: var(--space-3);
}
.btn:last-child {
margin-bottom: 0;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-500) 0%, var(--primary-600) 100%);
color: white;
box-shadow: 0 2px 8px rgba(49, 130, 206, 0.2);
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(49, 130, 206, 0.3);
}
.btn-primary:disabled {
background: var(--gray-300);
color: var(--gray-500);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn-secondary {
background: white;
color: var(--gray-600);
border: 2px solid var(--gray-200);
}
.btn-secondary:hover {
background: var(--gray-50);
border-color: var(--gray-300);
color: var(--gray-700);
}
/* Loading */
.loading {
position: relative;
pointer-events: none;
}
.loading::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
/* Skeleton */
.skeleton {
background: linear-gradient(90deg, var(--gray-200) 25%, var(--gray-100) 50%, var(--gray-200) 75%);
background-size: 200% 100%;
animation: skeleton 1.5s infinite;
border-radius: 8px;
}
.skeleton-product {
height: 140px;
margin-bottom: var(--space-4);
}
@keyframes skeleton {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Alert */
.alert {
padding: var(--space-4);
border-radius: 12px;
font-size: var(--text-sm);
margin-bottom: var(--space-4);
display: none;
}
.alert.show {
display: block;
}
.alert-error {
background-color: #FEF2F2;
border: 1px solid #FECACA;
color: #991B1B;
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<div class="header-content">
<button class="back-btn" onclick="goBack()" aria-label="뒤로가기">
</button>
<h1 class="page-title">상품 선택</h1>
</div>
</header>
<!-- Error Alert -->
<div id="errorAlert" class="alert alert-error">
<span id="errorMessage"></span>
</div>
<!-- Current Product Summary -->
<div class="current-product">
<div class="current-label">현재 이용 중인 상품</div>
<div class="current-name">5G 프리미엄 플랜</div>
<div class="current-price">월 69,000원</div>
</div>
<!-- Products Section -->
<div class="products-section">
<h2 class="section-title">변경 가능한 상품</h2>
<!-- Loading Skeletons (hidden by default) -->
<div id="loadingSkeletons" style="display: none;">
<div class="skeleton skeleton-product"></div>
<div class="skeleton skeleton-product"></div>
<div class="skeleton skeleton-product"></div>
</div>
<!-- Product List -->
<div id="productList">
<!-- Product Card 1 -->
<div class="product-card" data-product-id="basic" onclick="selectProduct(this)">
<div class="product-radio"></div>
<div class="product-header">
<div class="product-icon">📱</div>
<div class="product-info">
<div class="product-name">5G 베이직 플랜</div>
<div class="product-price">월 39,000원</div>
</div>
</div>
<div class="benefits-grid">
<div class="benefit-item">
<span class="benefit-icon"></span>
<span>5G 데이터 10GB</span>
</div>
<div class="benefit-item">
<span class="benefit-icon"></span>
<span>음성통화 300분</span>
</div>
<div class="benefit-item">
<span class="benefit-icon"></span>
<span>문자 무제한</span>
</div>
</div>
<div class="price-comparison">
<span>현재 상품 대비</span>
<span class="price-change price-down">월 30,000원 절약</span>
</div>
</div>
<!-- Product Card 2 -->
<div class="product-card" data-product-id="standard" onclick="selectProduct(this)">
<div class="product-radio"></div>
<div class="product-header">
<div class="product-icon">📱</div>
<div class="product-info">
<div class="product-name">5G 스탠다드 플랜</div>
<div class="product-price">월 59,000원</div>
</div>
</div>
<div class="benefits-grid">
<div class="benefit-item">
<span class="benefit-icon"></span>
<span>5G 데이터 50GB</span>
</div>
<div class="benefit-item">
<span class="benefit-icon"></span>
<span>음성통화 무제한</span>
</div>
<div class="benefit-item">
<span class="benefit-icon"></span>
<span>문자 무제한</span>
</div>
<div class="benefit-item">
<span class="benefit-icon"></span>
<span>해외 로밍 20% 할인</span>
</div>
</div>
<div class="price-comparison">
<span>현재 상품 대비</span>
<span class="price-change price-down">월 10,000원 절약</span>
</div>
</div>
<!-- Product Card 3 -->
<div class="product-card" data-product-id="unlimited" onclick="selectProduct(this)">
<div class="product-radio"></div>
<div class="product-header">
<div class="product-icon">📱</div>
<div class="product-info">
<div class="product-name">5G 언리미티드 플랜</div>
<div class="product-price">월 89,000원</div>
</div>
</div>
<div class="benefits-grid">
<div class="benefit-item">
<span class="benefit-icon"></span>
<span>5G 데이터 무제한</span>
</div>
<div class="benefit-item">
<span class="benefit-icon"></span>
<span>음성통화 무제한</span>
</div>
<div class="benefit-item">
<span class="benefit-icon"></span>
<span>문자 무제한</span>
</div>
<div class="benefit-item">
<span class="benefit-icon"></span>
<span>해외 로밍 무료</span>
</div>
<div class="benefit-item">
<span class="benefit-icon"></span>
<span>OTT 서비스 3개</span>
</div>
</div>
<div class="price-comparison">
<span>현재 상품 대비</span>
<span class="price-change price-up">월 20,000원 추가</span>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed Action Buttons -->
<div class="fixed-actions">
<button class="btn btn-primary" id="nextBtn" disabled onclick="goToRequest()">
선택한 상품으로 변경
</button>
<button class="btn btn-secondary" onclick="goBack()">
취소
</button>
</div>
<script>
let selectedProductId = null;
// Select product function
function selectProduct(cardElement) {
// Remove selection from all cards
document.querySelectorAll('.product-card').forEach(card => {
card.classList.remove('selected');
});
// Add selection to clicked card
cardElement.classList.add('selected');
// Store selected product ID
selectedProductId = cardElement.dataset.productId;
// Enable next button
const nextBtn = document.getElementById('nextBtn');
nextBtn.disabled = false;
// Update button text based on selection
const productName = cardElement.querySelector('.product-name').textContent;
nextBtn.textContent = `${productName}으로 변경`;
}
// Navigation functions
function goBack() {
window.history.back();
}
function goToRequest() {
if (!selectedProductId) {
showError('변경할 상품을 선택해주세요.');
return;
}
const nextBtn = document.getElementById('nextBtn');
// Show loading state
nextBtn.classList.add('loading');
nextBtn.disabled = true;
// Store selected product in sessionStorage
const selectedCard = document.querySelector(`[data-product-id="${selectedProductId}"]`);
const productData = {
id: selectedProductId,
name: selectedCard.querySelector('.product-name').textContent,
price: selectedCard.querySelector('.product-price').textContent,
benefits: Array.from(selectedCard.querySelectorAll('.benefit-item span:last-child')).map(el => el.textContent)
};
sessionStorage.setItem('selectedProduct', JSON.stringify(productData));
// Simulate loading
setTimeout(() => {
window.location.href = '07-상품변경요청.html';
}, 800);
}
// Show error message
function showError(message) {
const errorAlert = document.getElementById('errorAlert');
const errorMessage = document.getElementById('errorMessage');
errorMessage.textContent = message;
errorAlert.classList.add('show');
// Hide after 5 seconds
setTimeout(() => {
hideError();
}, 5000);
}
// Hide error message
function hideError() {
const errorAlert = document.getElementById('errorAlert');
errorAlert.classList.remove('show');
}
// Load products on page load
document.addEventListener('DOMContentLoaded', function() {
// Load current product from localStorage
loadCurrentProduct();
// Simulate loading products
loadProducts();
// Handle keyboard navigation
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
goBack();
}
if (e.key === 'Enter' && e.target.classList.contains('product-card')) {
selectProduct(e.target);
}
// Arrow key navigation for product cards
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
const cards = Array.from(document.querySelectorAll('.product-card'));
const currentIndex = cards.findIndex(card => card === document.activeElement);
if (e.key === 'ArrowDown' && currentIndex < cards.length - 1) {
cards[currentIndex + 1].focus();
e.preventDefault();
} else if (e.key === 'ArrowUp' && currentIndex > 0) {
cards[currentIndex - 1].focus();
e.preventDefault();
}
}
});
// Make product cards focusable for keyboard navigation
document.querySelectorAll('.product-card').forEach(card => {
card.setAttribute('tabindex', '0');
card.setAttribute('role', 'radio');
card.setAttribute('aria-checked', 'false');
});
});
// Load current product from localStorage
function loadCurrentProduct() {
try {
const currentProduct = localStorage.getItem('currentProduct');
if (currentProduct) {
const product = JSON.parse(currentProduct);
const currentNameElement = document.querySelector('.current-name');
if (currentNameElement && product.name) {
currentNameElement.textContent = product.name;
}
}
} catch (error) {
console.warn('localStorage에서 상품 정보를 불러오는 중 오류 발생:', error);
// 기본값 유지
}
}
function loadProducts() {
const loadingSkeletons = document.getElementById('loadingSkeletons');
const productList = document.getElementById('productList');
// Show loading
loadingSkeletons.style.display = 'block';
productList.style.display = 'none';
// Simulate API call
setTimeout(() => {
// Hide loading, show products
loadingSkeletons.style.display = 'none';
productList.style.display = 'block';
}, 1000);
}
// Update aria-checked when product is selected
function selectProduct(cardElement) {
// Remove selection from all cards
document.querySelectorAll('.product-card').forEach(card => {
card.classList.remove('selected');
card.setAttribute('aria-checked', 'false');
});
// Add selection to clicked card
cardElement.classList.add('selected');
cardElement.setAttribute('aria-checked', 'true');
// Store selected product ID
selectedProductId = cardElement.dataset.productId;
// Store selected product info in sessionStorage
const productName = cardElement.querySelector('.product-name').textContent;
const productPrice = cardElement.querySelector('.product-price').textContent;
sessionStorage.setItem('selectedProductName', productName);
sessionStorage.setItem('selectedProductPrice', productPrice);
// Enable next button
const nextBtn = document.getElementById('nextBtn');
nextBtn.disabled = false;
// Update button text based on selection
nextBtn.textContent = `${productName}으로 변경`;
}
</script>
</body>
</html>
@@ -0,0 +1,774 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>상품 변경 요청 - 통신요금 관리 서비스</title>
<style>
/* CSS Variables - Style Guide */
:root {
/* Primary Colors */
--primary-50: #EBF8FF;
--primary-100: #BEE3F8;
--primary-200: #90CDF4;
--primary-300: #63B3ED;
--primary-400: #4299E1;
--primary-500: #3182CE;
--primary-600: #2B77CB;
--primary-700: #2C5282;
--primary-800: #2A4365;
--primary-900: #1A365D;
/* Gray Colors */
--gray-50: #F9FAFB;
--gray-100: #F3F4F6;
--gray-200: #E5E7EB;
--gray-300: #D1D5DB;
--gray-400: #9CA3AF;
--gray-500: #6B7280;
--gray-600: #4B5563;
--gray-700: #374151;
--gray-800: #1F2937;
--gray-900: #111827;
/* Status Colors */
--success-500: #38A169;
--error-500: #E53E3E;
--warning-500: #ED8936;
/* Spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
/* Typography */
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
--text-4xl: 2.25rem;
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
}
/* Reset & Base */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
font-size: var(--text-base);
line-height: 1.5;
color: var(--gray-700);
background-color: var(--gray-50);
min-height: 100vh;
padding-bottom: 120px; /* Space for fixed buttons */
}
/* Container */
.container {
width: 100%;
max-width: 400px;
margin: 0 auto;
padding: var(--space-4);
background-color: white;
min-height: 100vh;
}
@media (min-width: 768px) {
.container {
max-width: 480px;
padding: var(--space-6);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
}
/* Header */
.header {
position: sticky;
top: 0;
background: white;
z-index: 10;
padding: var(--space-4) 0;
border-bottom: 1px solid var(--gray-200);
margin-bottom: var(--space-6);
}
.header-content {
display: flex;
align-items: center;
gap: var(--space-4);
}
.back-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: none;
background: none;
border-radius: 8px;
cursor: pointer;
font-size: var(--text-xl);
color: var(--gray-600);
transition: all 0.2s ease-in-out;
}
.back-btn:hover {
background-color: var(--gray-100);
color: var(--gray-800);
}
.page-title {
font-size: var(--text-xl);
font-weight: var(--font-bold);
color: var(--gray-900);
}
/* Card */
.card {
background-color: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
padding: var(--space-6);
margin-bottom: var(--space-6);
border: 1px solid var(--gray-200);
}
.card-title {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--gray-900);
margin-bottom: var(--space-4);
}
/* Change Comparison */
.change-comparison {
display: flex;
align-items: center;
gap: var(--space-4);
margin-bottom: var(--space-6);
}
.product-summary {
flex: 1;
padding: var(--space-4);
border-radius: 12px;
border: 2px solid var(--gray-200);
}
.product-summary.current {
background: var(--gray-50);
}
.product-summary.new {
background: linear-gradient(135deg, var(--primary-50) 0%, var(--primary-100) 100%);
border-color: var(--primary-200);
}
.product-label {
font-size: var(--text-xs);
color: var(--gray-500);
margin-bottom: var(--space-1);
font-weight: var(--font-medium);
text-transform: uppercase;
}
.product-name {
font-size: var(--text-base);
font-weight: var(--font-semibold);
color: var(--gray-900);
margin-bottom: var(--space-1);
}
.product-price {
font-size: var(--text-sm);
color: var(--primary-600);
font-weight: var(--font-medium);
}
.change-arrow {
font-size: var(--text-2xl);
color: var(--primary-500);
flex-shrink: 0;
}
/* Notice */
.notice {
background: #FFF7ED;
border: 1px solid #FED7AA;
border-radius: 12px;
padding: var(--space-4);
margin-bottom: var(--space-6);
}
.notice-title {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-sm);
font-weight: var(--font-semibold);
color: var(--warning-500);
margin-bottom: var(--space-3);
}
.notice-list {
list-style: none;
padding: 0;
}
.notice-item {
font-size: var(--text-sm);
color: #92400E;
line-height: 1.4;
margin-bottom: var(--space-2);
padding-left: var(--space-4);
position: relative;
}
.notice-item::before {
content: '•';
position: absolute;
left: 0;
color: var(--warning-500);
font-weight: bold;
}
.notice-item:last-child {
margin-bottom: 0;
}
/* Progress */
.progress-section {
margin-bottom: var(--space-8);
}
.progress-bar {
width: 100%;
height: 8px;
background: var(--gray-200);
border-radius: 4px;
overflow: hidden;
margin-bottom: var(--space-4);
}
.progress-fill {
height: 100%;
background: linear-gradient(135deg, var(--primary-500) 0%, var(--primary-600) 100%);
border-radius: 4px;
transition: width 0.3s ease-in-out;
width: 0%;
}
.progress-text {
text-align: center;
font-size: var(--text-sm);
color: var(--gray-600);
margin-bottom: var(--space-2);
}
.progress-status {
text-align: center;
font-size: var(--text-base);
font-weight: var(--font-semibold);
color: var(--primary-600);
}
.progress-steps {
display: flex;
justify-content: space-between;
margin-top: var(--space-4);
}
.progress-step {
flex: 1;
text-align: center;
font-size: var(--text-xs);
color: var(--gray-400);
position: relative;
}
.progress-step.active {
color: var(--primary-600);
font-weight: var(--font-semibold);
}
.progress-step.completed {
color: var(--success-500);
font-weight: var(--font-semibold);
}
/* Fixed Action Buttons */
.fixed-actions {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 400px;
background: white;
padding: var(--space-4);
border-top: 1px solid var(--gray-200);
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
}
@media (min-width: 768px) {
.fixed-actions {
max-width: 480px;
padding: var(--space-6);
}
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--space-4) var(--space-6);
border-radius: 12px;
font-size: var(--text-base);
font-weight: var(--font-semibold);
line-height: 1.5;
transition: all 0.2s ease-in-out;
cursor: pointer;
border: none;
min-height: 52px;
text-decoration: none;
width: 100%;
margin-bottom: var(--space-3);
}
.btn:last-child {
margin-bottom: 0;
}
.btn-primary {
background-color: #3182CE !important; /* Fallback color with important */
background: linear-gradient(135deg, #3182CE 0%, #2B77CB 100%) !important;
color: white !important;
box-shadow: 0 2px 8px rgba(49, 130, 206, 0.2);
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(49, 130, 206, 0.3);
}
.btn-primary:disabled {
background: var(--gray-300);
color: var(--gray-500);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn-secondary {
background: white;
color: var(--gray-600);
border: 2px solid var(--gray-200);
}
.btn-secondary:hover {
background: var(--gray-50);
border-color: var(--gray-300);
color: var(--gray-700);
}
.btn-outline {
background: transparent;
color: var(--primary-600);
border: 2px solid var(--primary-200);
}
.btn-outline:hover {
background: var(--primary-50);
border-color: var(--primary-300);
}
/* Loading */
.loading {
position: relative;
pointer-events: none;
}
.loading::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
/* Alert */
.alert {
padding: var(--space-4);
border-radius: 12px;
font-size: var(--text-sm);
margin-bottom: var(--space-4);
display: none;
}
.alert.show {
display: block;
}
.alert-error {
background-color: #FEF2F2;
border: 1px solid #FECACA;
color: #991B1B;
}
.alert-success {
background-color: #F0FDF4;
border: 1px solid #BBF7D0;
color: #166534;
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<div class="header-content">
<button class="back-btn" onclick="goBack()" aria-label="뒤로가기">
</button>
<h1 class="page-title">상품 변경 요청</h1>
</div>
</header>
<!-- Alerts -->
<div id="errorAlert" class="alert alert-error">
<span id="errorMessage"></span>
</div>
<div id="successAlert" class="alert alert-success">
<span id="successMessage"></span>
</div>
<!-- Change Confirmation -->
<div class="card">
<h2 class="card-title">변경 내용 확인</h2>
<div class="change-comparison">
<div class="product-summary current">
<div class="product-label">현재 상품</div>
<div class="product-name">5G 프리미엄 플랜</div>
<div class="product-price">월 69,000원</div>
</div>
<div class="change-arrow"></div>
<div class="product-summary new">
<div class="product-label">변경할 상품</div>
<div class="product-name" id="newProductName">5G 스탠다드 플랜</div>
<div class="product-price" id="newProductPrice">월 59,000원</div>
</div>
</div>
</div>
<!-- Important Notice -->
<div class="notice">
<div class="notice-title">
<span>⚠️</span>
<span>중요 안내사항</span>
</div>
<ul class="notice-list">
<li class="notice-item">상품 변경은 다음 월 1일부터 적용됩니다</li>
<li class="notice-item">현재 약정 기간이 남아있는 경우 위약금이 발생할 수 있습니다</li>
<li class="notice-item">기존 부가서비스는 자동으로 해지되며, 필요시 재신청해야 합니다</li>
<li class="notice-item">변경 후 14일 이내에 취소 가능하나, 일부 제약이 있을 수 있습니다</li>
<li class="notice-item">요금제 변경에 따른 데이터 이월은 불가능합니다</li>
</ul>
</div>
<!-- Progress Section -->
<div class="card progress-section">
<h2 class="card-title">사전 검증 진행 상황</h2>
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-text" id="progressText">검증을 시작하세요</div>
<div class="progress-status" id="progressStatus">검증 대기중</div>
<div class="progress-steps">
<div class="progress-step" data-step="1">
<div>약정 확인</div>
</div>
<div class="progress-step" data-step="2">
<div>자격 검증</div>
</div>
<div class="progress-step" data-step="3">
<div>요금 계산</div>
</div>
<div class="progress-step" data-step="4">
<div>승인 완료</div>
</div>
</div>
</div>
</div>
<!-- Fixed Action Buttons -->
<div class="fixed-actions">
<button class="btn btn-primary" id="submitBtn" disabled onclick="startValidation()">
사전 검증 시작
</button>
<button class="btn btn-secondary" onclick="goBack()">
취소
</button>
<button class="btn btn-outline" onclick="goToPrevious()">
이전 단계
</button>
</div>
<script>
let validationStep = 0;
let validationCompleted = false;
// Load selected product data from previous screen
document.addEventListener('DOMContentLoaded', function() {
loadSelectedProduct();
enableSubmitButton();
// Handle keyboard navigation
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
goBack();
}
});
});
function loadSelectedProduct() {
const selectedProduct = sessionStorage.getItem('selectedProduct');
if (selectedProduct) {
const product = JSON.parse(selectedProduct);
document.getElementById('newProductName').textContent = product.name;
document.getElementById('newProductPrice').textContent = product.price;
}
}
function enableSubmitButton() {
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = false;
}
function startValidation() {
const submitBtn = document.getElementById('submitBtn');
if (validationCompleted) {
// Already validated, proceed to result
goToResult();
return;
}
// Disable button during validation
submitBtn.disabled = true;
submitBtn.textContent = '검증 중...';
// Start validation process
runValidationSteps();
}
function runValidationSteps() {
const steps = [
{ name: '약정 확인', duration: 2000 },
{ name: '자격 검증', duration: 1500 },
{ name: '요금 계산', duration: 1800 },
{ name: '승인 완료', duration: 1000 }
];
let currentStep = 0;
function processNextStep() {
if (currentStep >= steps.length) {
onValidationComplete();
return;
}
const step = steps[currentStep];
const stepIndex = currentStep + 1;
// Update progress
updateProgress(stepIndex, steps.length, `${step.name} 진행 중...`);
// Mark current step as active
document.querySelectorAll('.progress-step').forEach((el, index) => {
if (index < stepIndex - 1) {
el.classList.add('completed');
el.classList.remove('active');
} else if (index === stepIndex - 1) {
el.classList.add('active');
el.classList.remove('completed');
} else {
el.classList.remove('active', 'completed');
}
});
// Simulate step processing
setTimeout(() => {
currentStep++;
processNextStep();
}, step.duration);
}
processNextStep();
}
function updateProgress(current, total, statusText) {
const progress = (current / total) * 100;
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
const progressStatus = document.getElementById('progressStatus');
progressFill.style.width = `${progress}%`;
progressText.textContent = `${current}/${total} 단계`;
progressStatus.textContent = statusText;
}
function onValidationComplete() {
validationCompleted = true;
// Update UI
const submitBtn = document.getElementById('submitBtn');
const progressStatus = document.getElementById('progressStatus');
updateProgress(4, 4, '모든 검증이 완료되었습니다');
// Mark all steps as completed
document.querySelectorAll('.progress-step').forEach(el => {
el.classList.add('completed');
el.classList.remove('active');
});
// Show success message
showSuccess('사전 검증이 성공적으로 완료되었습니다.');
// Enable submit button
submitBtn.disabled = false;
submitBtn.textContent = '변경 신청하기';
// Add some celebration effect
setTimeout(() => {
submitBtn.style.background = 'linear-gradient(135deg, var(--success-500) 0%, var(--success-600) 100%)';
}, 500);
}
function submitRequest() {
const progressStatus = document.getElementById('progressStatus');
const submitBtn = document.getElementById('submitBtn');
// Update status text
progressStatus.textContent = '상품 변경 요청을 처리하고 있습니다...';
// Show loading on button
submitBtn.classList.add('loading');
submitBtn.disabled = true;
submitBtn.textContent = '처리 중...';
// Start actual submission process
setTimeout(() => {
goToResult();
}, 800);
}
function goToResult() {
const submitBtn = document.getElementById('submitBtn');
// Show loading state
submitBtn.classList.add('loading');
submitBtn.disabled = true;
// Store request data
let currentProductName = '5G 프리미엄 플랜';
try {
const currentProduct = localStorage.getItem('currentProduct');
if (currentProduct) {
const product = JSON.parse(currentProduct);
currentProductName = product.name;
}
} catch (error) {
console.warn('localStorage에서 상품 정보를 불러오는 중 오류 발생:', error);
}
const requestData = {
currentProduct: currentProductName,
newProduct: sessionStorage.getItem('selectedProductName') || '5G 스탠다드 플랜',
newPrice: sessionStorage.getItem('selectedProductPrice') || '월 59,000원',
requestTime: new Date().toISOString(),
status: 'success'
};
sessionStorage.setItem('changeRequest', JSON.stringify(requestData));
// Simulate final processing
setTimeout(() => {
window.location.href = '08-처리결과화면.html';
}, 1500);
}
// Navigation functions
function goBack() {
window.history.back();
}
function goToPrevious() {
window.location.href = '06-상품변경화면.html';
}
// Alert functions
function showError(message) {
const errorAlert = document.getElementById('errorAlert');
const errorMessage = document.getElementById('errorMessage');
errorMessage.textContent = message;
errorAlert.classList.add('show');
setTimeout(() => {
hideError();
}, 5000);
}
function hideError() {
const errorAlert = document.getElementById('errorAlert');
errorAlert.classList.remove('show');
}
function showSuccess(message) {
const successAlert = document.getElementById('successAlert');
const successMessage = document.getElementById('successMessage');
successMessage.textContent = message;
successAlert.classList.add('show');
setTimeout(() => {
hideSuccess();
}, 3000);
}
function hideSuccess() {
const successAlert = document.getElementById('successAlert');
successAlert.classList.remove('show');
}
</script>
</body>
</html>
@@ -0,0 +1,739 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>처리 결과 - 통신요금 관리 서비스</title>
<style>
/* CSS Variables - Style Guide */
:root {
/* Primary Colors */
--primary-50: #EBF8FF;
--primary-100: #BEE3F8;
--primary-200: #90CDF4;
--primary-300: #63B3ED;
--primary-400: #4299E1;
--primary-500: #3182CE;
--primary-600: #2B77CB;
--primary-700: #2C5282;
--primary-800: #2A4365;
--primary-900: #1A365D;
/* Gray Colors */
--gray-50: #F9FAFB;
--gray-100: #F3F4F6;
--gray-200: #E5E7EB;
--gray-300: #D1D5DB;
--gray-400: #9CA3AF;
--gray-500: #6B7280;
--gray-600: #4B5563;
--gray-700: #374151;
--gray-800: #1F2937;
--gray-900: #111827;
/* Status Colors */
--success-50: #F0FDF4;
--success-100: #DCFCE7;
--success-500: #38A169;
--success-600: #2F855A;
--error-50: #FEF2F2;
--error-100: #FEE2E2;
--error-500: #E53E3E;
--error-600: #DC2626;
--warning-500: #ED8936;
/* Spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
/* Typography */
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
--text-4xl: 2.25rem;
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
}
/* Reset & Base */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
font-size: var(--text-base);
line-height: 1.5;
color: var(--gray-700);
background-color: var(--gray-50);
min-height: 100vh;
padding-bottom: 100px; /* Space for fixed buttons */
}
/* Container */
.container {
width: 100%;
max-width: 400px;
margin: 0 auto;
padding: var(--space-4);
background-color: white;
min-height: 100vh;
}
@media (min-width: 768px) {
.container {
max-width: 480px;
padding: var(--space-6);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
}
/* Header */
.header {
text-align: center;
padding: var(--space-6) 0;
border-bottom: 1px solid var(--gray-200);
margin-bottom: var(--space-8);
}
.page-title {
font-size: var(--text-2xl);
font-weight: var(--font-bold);
color: var(--gray-900);
}
/* Result Status */
.result-status {
text-align: center;
margin-bottom: var(--space-8);
padding: var(--space-8);
}
.result-icon {
width: 80px;
height: 80px;
margin: 0 auto var(--space-6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-4xl);
animation: scaleIn 0.6s ease-out;
}
.result-status.success .result-icon {
background: linear-gradient(135deg, var(--success-500) 0%, var(--success-600) 100%);
color: white;
box-shadow: 0 8px 25px rgba(56, 161, 105, 0.3);
}
.result-status.error .result-icon {
background: linear-gradient(135deg, var(--error-500) 0%, var(--error-600) 100%);
color: white;
box-shadow: 0 8px 25px rgba(229, 62, 62, 0.3);
}
@keyframes scaleIn {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
opacity: 1;
}
}
.result-title {
font-size: var(--text-2xl);
font-weight: var(--font-bold);
margin-bottom: var(--space-3);
}
.result-status.success .result-title {
color: var(--success-600);
}
.result-status.error .result-title {
color: var(--error-600);
}
.result-description {
font-size: var(--text-base);
color: var(--gray-600);
line-height: 1.6;
}
/* Card */
.card {
background-color: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
padding: var(--space-6);
margin-bottom: var(--space-6);
border: 1px solid var(--gray-200);
}
.card-title {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--gray-900);
margin-bottom: var(--space-4);
}
/* Success Details */
.success-details {
background: linear-gradient(135deg, var(--success-50) 0%, var(--success-100) 100%);
border: 2px solid var(--success-100);
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3) 0;
border-bottom: 1px solid rgba(56, 161, 105, 0.1);
}
.detail-row:last-child {
border-bottom: none;
}
.detail-label {
font-size: var(--text-sm);
color: var(--gray-600);
font-weight: var(--font-medium);
}
.detail-value {
font-size: var(--text-base);
font-weight: var(--font-semibold);
text-align: right;
}
.detail-value.product {
color: var(--primary-600);
}
.detail-value.date {
color: var(--success-600);
}
.detail-value.number {
color: var(--gray-900);
font-family: 'Courier New', monospace;
}
/* Error Details */
.error-details {
background: linear-gradient(135deg, var(--error-50) 0%, var(--error-100) 100%);
border: 2px solid var(--error-100);
}
.error-reason {
background: #FEF2F2;
border: 1px solid #FECACA;
border-radius: 12px;
padding: var(--space-4);
margin-bottom: var(--space-6);
}
.error-title {
font-size: var(--text-base);
font-weight: var(--font-semibold);
color: var(--error-600);
margin-bottom: var(--space-2);
}
.error-text {
font-size: var(--text-sm);
color: #991B1B;
line-height: 1.5;
}
.solution-steps {
list-style: none;
padding: 0;
}
.solution-step {
display: flex;
align-items: flex-start;
gap: var(--space-3);
margin-bottom: var(--space-3);
padding: var(--space-3);
background: white;
border-radius: 8px;
}
.step-number {
width: 24px;
height: 24px;
background: var(--primary-500);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-sm);
font-weight: var(--font-semibold);
flex-shrink: 0;
}
.step-text {
font-size: var(--text-sm);
color: var(--gray-700);
line-height: 1.5;
}
/* Contact Info */
.contact-info {
background: var(--gray-50);
border: 1px solid var(--gray-200);
border-radius: 12px;
padding: var(--space-4);
text-align: center;
}
.contact-title {
font-size: var(--text-base);
font-weight: var(--font-semibold);
color: var(--gray-900);
margin-bottom: var(--space-2);
}
.contact-number {
font-size: var(--text-xl);
font-weight: var(--font-bold);
color: var(--primary-600);
margin-bottom: var(--space-1);
}
.contact-hours {
font-size: var(--text-sm);
color: var(--gray-500);
}
/* Fixed Action Buttons */
.fixed-actions {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 400px;
background: white;
padding: var(--space-4);
border-top: 1px solid var(--gray-200);
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
}
@media (min-width: 768px) {
.fixed-actions {
max-width: 480px;
padding: var(--space-6);
}
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--space-4) var(--space-6);
border-radius: 12px;
font-size: var(--text-base);
font-weight: var(--font-semibold);
line-height: 1.5;
transition: all 0.2s ease-in-out;
cursor: pointer;
border: none;
min-height: 52px;
text-decoration: none;
width: 100%;
margin-bottom: var(--space-3);
}
.btn:last-child {
margin-bottom: 0;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-500) 0%, var(--primary-600) 100%);
color: white;
box-shadow: 0 2px 8px rgba(49, 130, 206, 0.2);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(49, 130, 206, 0.3);
}
.btn-secondary {
background: white;
color: var(--gray-600);
border: 2px solid var(--gray-200);
}
.btn-secondary:hover {
background: var(--gray-50);
border-color: var(--gray-300);
color: var(--gray-700);
}
.btn-success {
background: linear-gradient(135deg, var(--success-500) 0%, var(--success-600) 100%);
color: white;
box-shadow: 0 2px 8px rgba(56, 161, 105, 0.2);
}
.btn-success:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(56, 161, 105, 0.3);
}
.btn-error {
background: linear-gradient(135deg, var(--error-500) 0%, var(--error-600) 100%);
color: white;
box-shadow: 0 2px 8px rgba(229, 62, 62, 0.2);
}
.btn-error:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(229, 62, 62, 0.3);
}
/* Loading */
.loading {
position: relative;
pointer-events: none;
}
.loading::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
/* Hidden class for conditional display */
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<h1 class="page-title">처리 결과</h1>
</header>
<!-- Success Result -->
<div id="successResult" class="result-status success">
<div class="result-icon"></div>
<h2 class="result-title">상품 변경이 완료되었습니다</h2>
<p class="result-description">
선택하신 상품으로 변경 신청이 성공적으로 처리되었습니다.<br>
변경된 상품은 다음 월 1일부터 적용됩니다.
</p>
</div>
<!-- Error Result (Hidden by default) -->
<div id="errorResult" class="result-status error hidden">
<div class="result-icon"></div>
<h2 class="result-title">상품 변경에 실패했습니다</h2>
<p class="result-description">
죄송합니다. 상품 변경 처리 중 문제가 발생했습니다.<br>
아래의 해결 방법을 확인해주세요.
</p>
</div>
<!-- Success Details -->
<div id="successDetails" class="card success-details">
<h3 class="card-title">변경 내용</h3>
<div class="detail-row">
<span class="detail-label">변경된 상품</span>
<span class="detail-value product" id="changedProduct">5G 스탠다드 플랜</span>
</div>
<div class="detail-row">
<span class="detail-label">월 요금</span>
<span class="detail-value product" id="changedPrice">월 59,000원</span>
</div>
<div class="detail-row">
<span class="detail-label">적용일</span>
<span class="detail-value date">2025년 2월 1일</span>
</div>
<div class="detail-row">
<span class="detail-label">처리번호</span>
<span class="detail-value number" id="processNumber">CHG-2025010512345</span>
</div>
<div class="detail-row">
<span class="detail-label">처리일시</span>
<span class="detail-value" id="processTime">2025-01-05 14:23:45</span>
</div>
</div>
<!-- Error Details (Hidden by default) -->
<div id="errorDetails" class="card error-details hidden">
<h3 class="card-title">실패 사유</h3>
<div class="error-reason">
<div class="error-title">약정 위반으로 인한 변경 불가</div>
<div class="error-text">
현재 이용 중인 상품의 약정 기간이 남아있어 상품 변경이 불가능합니다.
약정 해지 후 다시 시도하시거나 고객센터로 문의해 주세요.
</div>
</div>
<h4 class="card-title">해결 방법</h4>
<ol class="solution-steps">
<li class="solution-step">
<span class="step-number">1</span>
<span class="step-text">현재 약정 상태와 위약금을 확인해보세요</span>
</li>
<li class="solution-step">
<span class="step-number">2</span>
<span class="step-text">약정 기간 만료 후 다시 시도하거나 위약금을 납부하고 변경하세요</span>
</li>
<li class="solution-step">
<span class="step-number">3</span>
<span class="step-text">고객센터에 문의하여 다른 변경 방법을 안내받으세요</span>
</li>
</ol>
<div class="contact-info">
<div class="contact-title">고객센터</div>
<div class="contact-number">1588-0000</div>
<div class="contact-hours">평일 09:00~18:00 (토/일/공휴일 휴무)</div>
</div>
</div>
</div>
<!-- Fixed Action Buttons -->
<div class="fixed-actions">
<!-- Success Actions -->
<div id="successActions">
<button class="btn btn-primary" onclick="goToMain()">
메인으로
</button>
<button class="btn btn-secondary" onclick="viewBill()">
요금 조회
</button>
</div>
<!-- Error Actions (Hidden by default) -->
<div id="errorActions" class="hidden">
<button class="btn btn-error" onclick="retryChange()">
다시 시도
</button>
<button class="btn btn-secondary" onclick="contactSupport()">
고객센터 연결
</button>
<button class="btn btn-secondary" onclick="goToMain()">
메인으로
</button>
</div>
</div>
<script>
// Load request data and determine result
document.addEventListener('DOMContentLoaded', function() {
loadResultData();
// Handle keyboard navigation
document.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && e.target.tagName === 'BUTTON') {
e.target.click();
}
});
});
function loadResultData() {
const requestData = sessionStorage.getItem('changeRequest');
// Simulate random success/failure for demo
// In real app, this would come from the API response
const isSuccess = Math.random() > 0.2; // 80% success rate for demo
if (requestData) {
const request = JSON.parse(requestData);
if (isSuccess) {
showSuccessResult(request);
} else {
showErrorResult();
}
} else {
// Default success display
showSuccessResult({
newProduct: '5G 스탠다드 플랜',
newPrice: '월 59,000원'
});
}
}
function showSuccessResult(requestData) {
// Show success elements
document.getElementById('successResult').classList.remove('hidden');
document.getElementById('successDetails').classList.remove('hidden');
document.getElementById('successActions').classList.remove('hidden');
// Hide error elements
document.getElementById('errorResult').classList.add('hidden');
document.getElementById('errorDetails').classList.add('hidden');
document.getElementById('errorActions').classList.add('hidden');
// Update success details
if (requestData.newProduct) {
document.getElementById('changedProduct').textContent = requestData.newProduct;
}
if (requestData.newPrice) {
document.getElementById('changedPrice').textContent = requestData.newPrice;
}
// Save successful product change to localStorage
const newProduct = {
name: requestData.newProduct || '5G 스탠다드 플랜',
price: requestData.newPrice || '월 59,000원',
benefits: getProductBenefits(requestData.newProduct),
changeDate: new Date().toISOString()
};
localStorage.setItem('currentProduct', JSON.stringify(newProduct));
// Generate process number and time
const processNumber = `CHG-${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(new Date().getDate()).padStart(2, '0')}${String(Math.floor(Math.random() * 99999)).padStart(5, '0')}`;
const processTime = new Date().toLocaleString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).replace(/\. /g, '-').replace('.', '');
document.getElementById('processNumber').textContent = processNumber;
document.getElementById('processTime').textContent = processTime;
}
// Get product benefits based on product name
function getProductBenefits(productName) {
const productBenefits = {
'5G 스탠다드 플랜': [
'5G 데이터 100GB',
'음성통화 무제한',
'문자 무제한',
'영상통화 300분'
],
'5G 베이직 플랜': [
'5G 데이터 50GB',
'음성통화 무제한',
'문자 무제한',
'영상통화 300분'
],
'5G 프리미엄 플랜': [
'5G 데이터 무제한',
'음성통화 무제한',
'문자 무제한',
'해외 로밍 50% 할인'
]
};
return productBenefits[productName] || [
'5G 데이터 무제한',
'음성통화 무제한',
'문자 무제한'
];
}
function showErrorResult() {
// Show error elements
document.getElementById('errorResult').classList.remove('hidden');
document.getElementById('errorDetails').classList.remove('hidden');
document.getElementById('errorActions').classList.remove('hidden');
// Hide success elements
document.getElementById('successResult').classList.add('hidden');
document.getElementById('successDetails').classList.add('hidden');
document.getElementById('successActions').classList.add('hidden');
}
// Navigation functions
function goToMain() {
const btn = event.target;
btn.classList.add('loading');
btn.disabled = true;
setTimeout(() => {
window.location.href = '02-메인화면.html';
}, 500);
}
function viewBill() {
const btn = event.target;
btn.classList.add('loading');
btn.disabled = true;
setTimeout(() => {
window.location.href = '03-요금조회메뉴.html';
}, 500);
}
function retryChange() {
const btn = event.target;
btn.classList.add('loading');
btn.disabled = true;
// Clear previous data
sessionStorage.removeItem('changeRequest');
sessionStorage.removeItem('selectedProduct');
setTimeout(() => {
window.location.href = '05-상품변경메뉴.html';
}, 800);
}
function contactSupport() {
// Simulate contacting support
alert('고객센터 연결 기능입니다.\n\n전화번호: 1588-0000\n운영시간: 평일 09:00~18:00');
}
// Auto-clear session data after 30 minutes to prevent stale data
setTimeout(() => {
sessionStorage.removeItem('changeRequest');
sessionStorage.removeItem('selectedProduct');
}, 30 * 60 * 1000); // 30 minutes
</script>
</body>
</html>
+743
View File
@@ -0,0 +1,743 @@
# 통신요금 관리 서비스 - 스타일 가이드
- [통신요금 관리 서비스 - 스타일 가이드](#통신요금-관리-서비스---스타일-가이드)
- [브랜드 아이덴티티](#브랜드-아이덴티티)
- [디자인 원칙](#디자인-원칙)
- [컬러 시스템](#컬러-시스템)
- [타이포그래피](#타이포그래피)
- [간격 시스템](#간격-시스템)
- [컴포넌트 스타일](#컴포넌트-스타일)
- [반응형 브레이크포인트](#반응형-브레이크포인트)
- [대상 서비스 특화 컴포넌트](#대상-서비스-특화-컴포넌트)
- [인터랙션 패턴](#인터랙션-패턴)
- [변경 이력](#변경-이력)
---
## 브랜드 아이덴티티
### 서비스 컨셉
- **키워드**: 신뢰성, 편리함, 명확성
- **브랜드 메시지**: "간편하고 안전한 통신요금 관리"
- **타겟**: 일반 MVNO 고객 (20대~60대)
### 디자인 컨셉
- **미니멀리즘**: 불필요한 요소 제거, 핵심 기능 집중
- **명확성 우선**: 정보 전달의 명확성과 가독성
- **안정감**: 금융 서비스의 신뢰성과 보안성 강조
- **접근성**: 모든 사용자가 편리하게 이용할 수 있는 인터페이스
---
## 디자인 원칙
### 1. 명확성 (Clarity)
- 모든 UI 요소는 그 목적이 명확해야 함
- 전문용어 사용 최소화, 일반적인 표현 우선
- 중요 정보는 시각적으로 강조
### 2. 일관성 (Consistency)
- 동일한 요소는 동일한 스타일 적용
- 예측 가능한 인터랙션 패턴
- 통일된 색상과 타이포그래피 사용
### 3. 효율성 (Efficiency)
- 최소한의 클릭으로 목표 달성
- 불필요한 단계 제거
- 빠른 로딩과 반응성 보장
### 4. 안전성 (Safety)
- 중요한 액션에는 확인 단계 제공
- 오류 방지와 명확한 피드백
- 개인정보 보호 강조
### 5. 포용성 (Inclusivity)
- 접근성 지침 준수
- 다양한 디바이스와 환경 지원
- 사용자 능력과 상황 고려
---
## 컬러 시스템
### Primary Colors
```css
/* 메인 브랜드 컬러 - 신뢰감을 주는 블루 */
--primary-50: #EBF8FF;
--primary-100: #BEE3F8;
--primary-200: #90CDF4;
--primary-300: #63B3ED;
--primary-400: #4299E1;
--primary-500: #3182CE; /* Main Brand Color */
--primary-600: #2B77CB;
--primary-700: #2C5282;
--primary-800: #2A4365;
--primary-900: #1A365D;
```
### Secondary Colors
```css
/* 보조 컬러 - 포인트 및 상태 표시 */
--secondary-50: #F7FAFC;
--secondary-100: #EDF2F7;
--secondary-200: #E2E8F0;
--secondary-300: #CBD5E0;
--secondary-400: #A0AEC0;
--secondary-500: #718096;
--secondary-600: #4A5568;
--secondary-700: #2D3748;
--secondary-800: #1A202C;
--secondary-900: #171923;
```
### Status Colors
```css
/* 성공 - 그린 */
--success-50: #F0FFF4;
--success-100: #C6F6D5;
--success-500: #38A169;
--success-600: #2F855A;
/* 경고 - 오렌지 */
--warning-50: #FFFAF0;
--warning-100: #FEEBC8;
--warning-500: #ED8936;
--warning-600: #DD6B20;
/* 오류 - 레드 */
--error-50: #FED7D7;
--error-100: #FED7D7;
--error-500: #E53E3E;
--error-600: #C53030;
/* 정보 - 블루 */
--info-50: #EBF8FF;
--info-100: #BEE3F8;
--info-500: #3182CE;
--info-600: #2B77CB;
```
### Neutral Colors
```css
/* 텍스트 및 배경 */
--gray-50: #F9FAFB; /* Background Light */
--gray-100: #F3F4F6; /* Background */
--gray-200: #E5E7EB; /* Border Light */
--gray-300: #D1D5DB; /* Border */
--gray-400: #9CA3AF; /* Text Muted */
--gray-500: #6B7280; /* Text Secondary */
--gray-600: #4B5563; /* Text Primary Light */
--gray-700: #374151; /* Text Primary */
--gray-800: #1F2937; /* Text Primary Dark */
--gray-900: #111827; /* Text Emphasis */
```
### 컬러 사용 가이드
- **Primary**: 주요 액션 버튼, 링크, 브랜드 요소
- **Secondary**: 보조 버튼, 아이콘, 경계선
- **Success**: 성공 메시지, 완료 상태
- **Warning**: 주의 메시지, 중요 알림
- **Error**: 오류 메시지, 실패 상태
- **Gray**: 텍스트, 배경, 구분선
---
## 타이포그래피
### 폰트 패밀리
```css
/* 기본 폰트 스택 */
font-family:
'Noto Sans KR', /* 한글 */
'Roboto', /* 영문 */
-apple-system,
BlinkMacSystemFont,
'Apple SD Gothic Neo',
'Malgun Gothic',
sans-serif;
```
### 폰트 크기 및 행간
```css
/* Heading */
--text-4xl: 2.25rem; /* 36px - Page Title */
--text-3xl: 1.875rem; /* 30px - Section Title */
--text-2xl: 1.5rem; /* 24px - Card Title */
--text-xl: 1.25rem; /* 20px - Sub Title */
--text-lg: 1.125rem; /* 18px - Large Text */
/* Body */
--text-base: 1rem; /* 16px - Body Text */
--text-sm: 0.875rem; /* 14px - Small Text */
--text-xs: 0.75rem; /* 12px - Caption */
/* Line Height */
--leading-tight: 1.25; /* Heading */
--leading-normal: 1.5; /* Body */
--leading-relaxed: 1.625; /* Long Text */
```
### 폰트 두께
```css
--font-light: 300; /* Light text */
--font-normal: 400; /* Body text */
--font-medium: 500; /* Emphasis */
--font-semibold: 600; /* Sub heading */
--font-bold: 700; /* Heading */
```
### 타이포그래피 클래스
```css
/* Heading Styles */
.heading-1 { font-size: 2.25rem; font-weight: 700; line-height: 1.25; }
.heading-2 { font-size: 1.875rem; font-weight: 600; line-height: 1.25; }
.heading-3 { font-size: 1.5rem; font-weight: 600; line-height: 1.25; }
.heading-4 { font-size: 1.25rem; font-weight: 500; line-height: 1.25; }
/* Body Styles */
.body-large { font-size: 1.125rem; font-weight: 400; line-height: 1.5; }
.body-normal { font-size: 1rem; font-weight: 400; line-height: 1.5; }
.body-small { font-size: 0.875rem; font-weight: 400; line-height: 1.5; }
.caption { font-size: 0.75rem; font-weight: 400; line-height: 1.25; }
/* Emphasis */
.text-emphasis { font-weight: 600; color: var(--gray-900); }
.text-muted { color: var(--gray-500); }
```
---
## 간격 시스템
### 기본 간격 단위 (8px 그리드 시스템)
```css
--space-0: 0;
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-5: 1.25rem; /* 20px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-10: 2.5rem; /* 40px */
--space-12: 3rem; /* 48px */
--space-16: 4rem; /* 64px */
--space-20: 5rem; /* 80px */
```
### 컴포넌트별 간격 가이드
- **Component Padding**: 16px (space-4) - 24px (space-6)
- **Content Margin**: 16px (space-4) - 32px (space-8)
- **Section Gap**: 32px (space-8) - 48px (space-12)
- **Page Padding**: 20px (space-5) - 40px (space-10)
### 레이아웃 간격
```css
/* Container */
--container-padding-mobile: var(--space-4); /* 16px */
--container-padding-tablet: var(--space-6); /* 24px */
--container-padding-desktop: var(--space-8); /* 32px */
/* Grid Gap */
--grid-gap-small: var(--space-4); /* 16px */
--grid-gap-medium: var(--space-6); /* 24px */
--grid-gap-large: var(--space-8); /* 32px */
```
---
## 컴포넌트 스타일
### 버튼 (Button)
```css
/* Base Button */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--space-3) var(--space-6);
border-radius: 8px;
font-size: var(--text-base);
font-weight: var(--font-medium);
line-height: 1.5;
transition: all 0.2s ease-in-out;
cursor: pointer;
border: none;
min-height: 44px; /* 터치 접근성 */
}
/* Primary Button */
.btn-primary {
background-color: var(--primary-500);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-600);
}
.btn-primary:disabled {
background-color: var(--gray-300);
color: var(--gray-500);
cursor: not-allowed;
}
/* Secondary Button */
.btn-secondary {
background-color: white;
color: var(--gray-700);
border: 1px solid var(--gray-300);
}
.btn-secondary:hover {
background-color: var(--gray-50);
border-color: var(--gray-400);
}
/* Danger Button */
.btn-danger {
background-color: var(--error-500);
color: white;
}
.btn-danger:hover {
background-color: var(--error-600);
}
```
### 카드 (Card)
```css
.card {
background-color: white;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: var(--space-6);
border: 1px solid var(--gray-200);
}
.card-header {
margin-bottom: var(--space-4);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--gray-200);
}
.card-title {
font-size: var(--text-xl);
font-weight: var(--font-semibold);
color: var(--gray-900);
margin: 0;
}
.card-content {
color: var(--gray-700);
}
```
### 폼 요소 (Form)
```css
/* Input */
.input {
width: 100%;
padding: var(--space-3) var(--space-4);
border: 1px solid var(--gray-300);
border-radius: 8px;
font-size: var(--text-base);
line-height: 1.5;
transition: border-color 0.2s ease-in-out;
min-height: 44px;
}
.input:focus {
outline: none;
border-color: var(--primary-500);
box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.1);
}
.input.error {
border-color: var(--error-500);
}
/* Label */
.label {
display: block;
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--gray-700);
margin-bottom: var(--space-2);
}
/* Select */
.select {
appearance: none;
background-image: url("data:image/svg+xml,..."); /* 드롭다운 아이콘 */
background-repeat: no-repeat;
background-position: right var(--space-3) center;
background-size: 16px;
}
```
### 알림 메시지 (Alert)
```css
.alert {
padding: var(--space-4);
border-radius: 8px;
border-left: 4px solid;
margin-bottom: var(--space-4);
}
.alert-success {
background-color: var(--success-50);
border-color: var(--success-500);
color: var(--success-800);
}
.alert-warning {
background-color: var(--warning-50);
border-color: var(--warning-500);
color: var(--warning-800);
}
.alert-error {
background-color: var(--error-50);
border-color: var(--error-500);
color: var(--error-800);
}
.alert-info {
background-color: var(--info-50);
border-color: var(--info-500);
color: var(--info-800);
}
```
---
## 반응형 브레이크포인트
### 브레이크포인트 정의
```css
/* Mobile First Approach */
:root {
--breakpoint-sm: 640px; /* Small devices */
--breakpoint-md: 768px; /* Medium devices */
--breakpoint-lg: 1024px; /* Large devices */
--breakpoint-xl: 1280px; /* Extra large devices */
}
/* Media Query Mixins */
@media (min-width: 640px) { /* sm */ }
@media (min-width: 768px) { /* md */ }
@media (min-width: 1024px) { /* lg */ }
@media (min-width: 1280px) { /* xl */ }
```
### 반응형 컨테이너
```css
.container {
width: 100%;
margin: 0 auto;
padding: 0 var(--space-4);
}
@media (min-width: 640px) {
.container {
max-width: 640px;
padding: 0 var(--space-6);
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
padding: 0 var(--space-8);
}
}
```
### 반응형 그리드
```css
.grid {
display: grid;
gap: var(--space-4);
grid-template-columns: 1fr; /* Mobile: 1 column */
}
@media (min-width: 768px) {
.grid-md-2 {
grid-template-columns: repeat(2, 1fr); /* Tablet: 2 columns */
}
}
@media (min-width: 1024px) {
.grid-lg-3 {
grid-template-columns: repeat(3, 1fr); /* Desktop: 3 columns */
}
}
```
---
## 대상 서비스 특화 컴포넌트
### 요금 정보 카드 (Bill Card)
```css
.bill-card {
background: linear-gradient(135deg, var(--primary-500) 0%, var(--primary-600) 100%);
color: white;
padding: var(--space-6);
border-radius: 16px;
box-shadow: 0 4px 20px rgba(49, 130, 206, 0.2);
}
.bill-amount {
font-size: var(--text-4xl);
font-weight: var(--font-bold);
margin-bottom: var(--space-2);
}
.bill-period {
font-size: var(--text-sm);
opacity: 0.8;
}
.bill-details {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: var(--space-4);
margin-top: var(--space-4);
}
```
### 상품 비교 카드 (Product Card)
```css
.product-card {
border: 2px solid var(--gray-200);
border-radius: 12px;
padding: var(--space-6);
transition: all 0.3s ease;
position: relative;
}
.product-card.selected {
border-color: var(--primary-500);
box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.1);
}
.product-card.current {
background-color: var(--success-50);
border-color: var(--success-500);
}
.product-badge {
position: absolute;
top: -8px;
right: var(--space-4);
background-color: var(--primary-500);
color: white;
padding: var(--space-1) var(--space-3);
border-radius: 999px;
font-size: var(--text-xs);
font-weight: var(--font-medium);
}
.product-price {
font-size: var(--text-2xl);
font-weight: var(--font-bold);
color: var(--primary-600);
}
```
### 진행 상태 표시 (Progress)
```css
.progress-container {
background-color: var(--gray-100);
border-radius: 999px;
height: 8px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--primary-500) 0%, var(--primary-400) 100%);
border-radius: 999px;
transition: width 0.3s ease;
}
.progress-steps {
display: flex;
justify-content: space-between;
margin-bottom: var(--space-4);
}
.progress-step {
display: flex;
align-items: center;
font-size: var(--text-sm);
color: var(--gray-500);
}
.progress-step.active {
color: var(--primary-600);
font-weight: var(--font-medium);
}
.progress-step.completed {
color: var(--success-600);
}
```
### 상태 뱃지 (Status Badge)
```css
.status-badge {
display: inline-flex;
align-items: center;
padding: var(--space-1) var(--space-3);
border-radius: 999px;
font-size: var(--text-xs);
font-weight: var(--font-medium);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.status-badge.processing {
background-color: var(--warning-100);
color: var(--warning-800);
}
.status-badge.completed {
background-color: var(--success-100);
color: var(--success-800);
}
.status-badge.failed {
background-color: var(--error-100);
color: var(--error-800);
}
.status-badge::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background-color: currentColor;
margin-right: var(--space-2);
}
```
---
## 인터랙션 패턴
### 애니메이션 타이밍
```css
:root {
--duration-fast: 0.15s;
--duration-normal: 0.3s;
--duration-slow: 0.5s;
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
}
```
### 호버 효과
```css
.interactive:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all var(--duration-normal) var(--ease-out);
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
```
### 로딩 상태
```css
.loading {
position: relative;
pointer-events: none;
opacity: 0.6;
}
.loading::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 2px solid var(--gray-300);
border-top: 2px solid var(--primary-500);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
```
### 포커스 상태
```css
.focusable:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.3);
border-radius: 8px;
}
.focus-visible {
outline: 2px solid var(--primary-500);
outline-offset: 2px;
}
```
### 상태 전환
```css
.fade-enter {
opacity: 0;
}
.fade-enter-active {
opacity: 1;
transition: opacity var(--duration-normal) var(--ease-out);
}
.slide-enter {
transform: translateX(100%);
}
.slide-enter-active {
transform: translateX(0);
transition: transform var(--duration-normal) var(--ease-out);
}
```
---
## 변경 이력
| 버전 | 날짜 | 변경사항 | 작성자 |
|------|------|----------|--------|
| 1.0 | 2025-01-05 | 초기 스타일 가이드 작성 | 박화면 |
---
## 스타일 가이드 활용 방법
### CSS 변수 사용
모든 스타일 정의에서 CSS 변수를 사용하여 일관성을 유지하고 쉬운 테마 변경을 지원합니다.
### 컴포넌트 기반 설계
재사용 가능한 컴포넌트 스타일을 정의하여 개발 효율성과 일관성을 높입니다.
### 접근성 고려
모든 컴포넌트는 WCAG 2.1 AA 기준을 준수하여 접근성을 보장합니다.
### 반응형 우선
Mobile First 접근 방식으로 모든 디바이스에서 최적의 사용자 경험을 제공합니다.
+578
View File
@@ -0,0 +1,578 @@
# 통신요금 관리 서비스 - UI/UX 설계서
- [통신요금 관리 서비스 - UI/UX 설계서](#통신요금-관리-서비스---uiux-설계서)
- [프로젝트 개요](#프로젝트-개요)
- [정보 아키텍처](#정보-아키텍처)
- [프로토타입 화면 목록](#프로토타입-화면-목록)
- [사용자 플로우](#사용자-플로우)
- [화면별 상세 설계](#화면별-상세-설계)
- [화면간 전환 및 네비게이션](#화면간-전환-및-네비게이션)
- [반응형 설계 전략](#반응형-설계-전략)
- [접근성 보장 방안](#접근성-보장-방안)
- [성능 최적화 방안](#성능-최적화-방안)
- [변경 이력](#변경-이력)
---
## 프로젝트 개요
### 서비스 목적
MVNO 고객의 통신요금 조회 및 상품변경을 지원하는 웹 서비스
### 주요 기능
1. **사용자 인증**: 안전한 로그인/로그아웃
2. **요금 조회**: 월별 통신요금 조회
3. **상품 변경**: 요금제 변경 요청 및 처리
### 설계 기준
- **유저스토리 기반**: 총 10개 유저스토리 100% 반영
- **B2C 웹 서비스**: 일반 고객 대상
- **보안 우선**: 개인정보 및 금융정보 보호
- **사용성 중심**: 직관적이고 간단한 UI/UX
---
## 정보 아키텍처
### 서비스 구조
```
통신요금 관리 서비스
├── 인증 영역
│ ├── 로그인
│ └── 권한 확인
├── 요금 조회 영역
│ ├── 조회 메뉴
│ ├── 조회 신청
│ └── 조회 결과
└── 상품 변경 영역
├── 변경 메뉴
├── 변경 화면
├── 변경 요청
└── 처리 결과
```
### 네비게이션 구조
```
메인 화면
├── 요금 조회 메뉴 → 요금 조회 신청 → 조회 결과
└── 상품 변경 메뉴 → 상품 변경 화면 → 변경 요청 → 처리 결과
```
---
## 프로토타입 화면 목록
| 화면 ID | 화면명 | 관련 유저스토리 | 우선순위 |
|---------|--------|-----------------|----------|
| SCR-001 | 로그인 | UFR-AUTH-010 | M |
| SCR-002 | 메인 화면 | UFR-AUTH-020 | M |
| SCR-003 | 요금조회 메뉴 | UFR-BILL-010 | M |
| SCR-004 | 요금조회 결과 | UFR-BILL-020, UFR-BILL-030, UFR-BILL-040 | M |
| SCR-005 | 상품변경 메뉴 | UFR-PROD-010 | M |
| SCR-006 | 상품변경 화면 | UFR-PROD-020 | M |
| SCR-007 | 상품변경 요청 | UFR-PROD-030 | M |
| SCR-008 | 처리결과 화면 | UFR-PROD-040 | M |
---
## 사용자 플로우
### 메인 플로우
```mermaid
flowchart TD
A[로그인] --> B[메인 화면]
B --> C[요금 조회]
B --> D[상품 변경]
C --> C1[요금조회 메뉴]
C1 --> C2[조회월 선택]
C2 --> C3[요금조회 결과]
C3 --> B
D --> D1[상품변경 메뉴]
D1 --> D2[상품변경 화면]
D2 --> D3[상품 선택]
D3 --> D4[변경 요청]
D4 --> D5[처리결과]
D5 --> B
```
### 오류 처리 플로우
```mermaid
flowchart TD
E1[로그인 실패] --> E1_1[오류 메시지 표시] --> E1_2[로그인 재시도]
E2[권한 없음] --> E2_1[권한 오류 메시지] --> E2_2[메인 화면]
E3[조회 실패] --> E3_1[조회 오류 메시지] --> E3_2[조회 메뉴]
E4[변경 실패] --> E4_1[변경 오류 메시지] --> E4_2[변경 화면]
```
---
## 화면별 상세 설계
### SCR-001: 로그인
**개요**
- 목적: 사용자 인증 및 서비스 접근
- 관련 유저스토리: UFR-AUTH-010
- 비즈니스 중요도: M/5
**주요 기능**
- ID/Password 입력
- 자동 로그인 옵션
- 로그인 버튼
- 오류 메시지 표시
**UI 구성요소**
```
Header
├── 서비스 로고
└── 서비스 제목
Main Content
├── 로그인 폼
│ ├── ID 입력 필드 (required)
│ ├── Password 입력 필드 (required, type=password)
│ ├── 자동 로그인 체크박스
│ └── 로그인 버튼 (primary)
└── 오류 메시지 영역
Footer
└── 저작권 정보
```
**인터랙션**
- ID/Password 유효성 검사 (실시간)
- 로그인 버튼 활성화/비활성화
- 5회 실패 시 계정 잠금 안내
- 성공 시 메인 화면 이동
---
### SCR-002: 메인 화면
**개요**
- 목적: 서비스 메뉴 제공 및 권한별 접근 제어
- 관련 유저스토리: UFR-AUTH-020
- 비즈니스 중요도: M/3
**주요 기능**
- 사용자 정보 표시 (회선번호)
- 서비스 메뉴 제공
- 권한별 메뉴 표시/숨김
**UI 구성요소**
```
Header
├── 서비스 로고
├── 사용자 정보 (회선번호)
└── 로그아웃 버튼
Main Content
├── 환영 메시지
└── 서비스 메뉴 그리드
├── 요금 조회 카드 (권한 확인)
└── 상품 변경 카드 (권한 확인)
Footer
└── 저작권 정보
```
**인터랙션**
- 권한 확인 후 메뉴 표시
- 권한 없는 메뉴는 비활성화 또는 숨김
- 카드 호버 효과
- 카드 클릭 시 해당 서비스로 이동
---
### SCR-003: 요금조회 메뉴
**개요**
- 목적: 요금 조회 옵션 제공
- 관련 유저스토리: UFR-BILL-010
- 비즈니스 중요도: M/5
**주요 기능**
- 회선번호 표시
- 조회월 선택 옵션
- 조회 신청 기능
**UI 구성요소**
```
Header
├── 뒤로가기 버튼
└── 페이지 제목 "요금 조회"
Main Content
├── 고객 정보 섹션
│ └── 회선번호 표시
├── 조회 옵션 섹션
│ ├── 조회월 선택 (드롭다운)
│ │ ├── 기본값: "현재 월"
│ │ └── 이전 6개월 옵션
│ └── 안내 텍스트
└── 액션 버튼 그룹
├── 조회 버튼 (primary)
└── 취소 버튼 (secondary)
```
**인터랙션**
- 조회월 드롭다운 선택
- 조회 버튼 클릭 시 로딩 상태
- 오류 시 에러 메시지 표시
---
### SCR-004: 요금조회 결과
**개요**
- 목적: 조회된 요금 정보 표시
- 관련 유저스토리: UFR-BILL-020, UFR-BILL-030, UFR-BILL-040
- 비즈니스 중요도: M/8, M/13, M/8
**주요 기능**
- 요금 정보 상세 표시
- 사용량 정보 제공
- 새로운 조회 기능
**UI 구성요소**
```
Header
├── 뒤로가기 버튼
└── 페이지 제목 "요금 조회 결과"
Main Content
├── 요금 정보 카드
│ ├── 청구월
│ ├── 상품명 (요금제)
│ ├── 총 요금 (강조 표시)
│ ├── 할인 정보
│ └── 약정 정보
├── 사용량 정보 카드
│ ├── 통화 사용량
│ ├── 데이터 사용량
│ └── SMS 사용량
├── 부가 정보 카드
│ ├── 단말기 할부금
│ ├── 예상 해지비용
│ └── 청구/납부 정보
└── 액션 버튼 그룹
├── 다른 월 조회 버튼
└── 메인으로 버튼
```
**인터랙션**
- 정보 카드 접기/펼치기
- 다른 월 조회 클릭 시 조회 메뉴로 이동
- 로딩 중 스켈레톤 UI 표시
---
### SCR-005: 상품변경 메뉴
**개요**
- 목적: 상품 변경 진입점 제공
- 관련 유저스토리: UFR-PROD-010
- 비즈니스 중요도: M/5
**주요 기능**
- 고객 정보 표시
- 현재 상품 정보 표시
- 상품 변경 화면으로 이동
**UI 구성요소**
```
Header
├── 뒤로가기 버튼
└── 페이지 제목 "상품 변경"
Main Content
├── 고객 정보 카드
│ ├── 회선번호
│ └── 고객ID
├── 현재 상품 정보 카드
│ ├── 상품명
│ ├── 월 기본료
│ └── 주요 혜택
├── 안내 메시지
│ └── 상품 변경 시 주의사항
└── 액션 버튼 그룹
├── 상품 변경하기 버튼 (primary)
└── 취소 버튼 (secondary)
```
**인터랙션**
- 현재 상품 정보 로딩
- 상품 변경하기 클릭 시 변경 화면으로 이동
- 로딩 실패 시 에러 메시지
---
### SCR-006: 상품변경 화면
**개요**
- 목적: 변경 가능한 상품 목록 제공
- 관련 유저스토리: UFR-PROD-020
- 비즈니스 중요도: M/8
**주요 기능**
- 변경 가능한 상품 목록 표시
- 상품 비교 기능
- 상품 선택 기능
**UI 구성요소**
```
Header
├── 뒤로가기 버튼
└── 페이지 제목 "상품 선택"
Main Content
├── 현재 상품 요약 (고정)
├── 상품 목록 섹션
│ └── 상품 카드들
│ ├── 상품명
│ ├── 월 기본료
│ ├── 주요 혜택 리스트
│ ├── 현재 상품과 비교
│ └── 선택 라디오 버튼
└── 액션 버튼 그룹 (고정)
├── 선택한 상품으로 변경 (primary, disabled)
└── 취소 버튼 (secondary)
```
**인터랙션**
- 상품 선택 시 버튼 활성화
- 상품 카드 선택 상태 시각화
- 스크롤 시 헤더와 버튼 고정
- 상품 로딩 중 스켈레톤 표시
---
### SCR-007: 상품변경 요청
**개요**
- 목적: 선택한 상품으로 변경 요청 확인
- 관련 유저스토리: UFR-PROD-030
- 비즈니스 중요도: M/13
**주요 기능**
- 변경 내용 확인
- 사전 체크 진행 상황
- 변경 요청 최종 실행
**UI 구성요소**
```
Header
├── 뒤로가기 버튼
└── 페이지 제목 "상품 변경 요청"
Main Content
├── 변경 내용 확인 카드
│ ├── 현재 상품
│ ├── 변경 화살표 아이콘
│ └── 변경할 상품
├── 주의사항 섹션
│ ├── 변경 시 주의사항
│ ├── 약정/할부 안내
│ └── 요금 변경 안내
├── 진행 상황 표시
│ ├── 사전 체크 진행 바
│ └── 상태 메시지
└── 액션 버튼 그룹
├── 변경 신청 버튼 (primary)
├── 취소 버튼 (secondary)
└── 이전 단계 버튼
```
**인터랙션**
- 사전 체크 진행 상태 실시간 업데이트
- 체크 완료 후 변경 신청 버튼 활성화
- 체크 실패 시 오류 메시지 및 재시도 옵션
---
### SCR-008: 처리결과 화면
**개요**
- 목적: 상품 변경 처리 결과 표시
- 관련 유저스토리: UFR-PROD-040
- 비즈니스 중요도: M/21
**주요 기능**
- 처리 결과 상태 표시
- 상세 처리 내용 제공
- 후속 액션 안내
**UI 구성요소**
```
Header
└── 페이지 제목 "처리 결과"
Main Content
├── 결과 상태 카드
│ ├── 성공/실패 아이콘 (대형)
│ ├── 결과 메시지 (제목)
│ └── 상태 설명
├── 처리 내용 카드 (성공 시)
│ ├── 변경된 상품 정보
│ ├── 적용일
│ └── 처리 번호
├── 실패 사유 카드 (실패 시)
│ ├── 실패 원인
│ ├── 해결 방법
│ └── 고객센터 안내
└── 액션 버튼 그룹
├── 메인으로 버튼 (primary)
├── 다시 시도 버튼 (실패 시)
└── 고객센터 연결 (실패 시)
```
**인터랙션**
- 결과에 따른 적절한 UI 표시
- 성공/실패 상태별 차별화된 컬러 스킴
- 추가 액션 버튼 제공
---
## 화면간 전환 및 네비게이션
### 네비게이션 패턴
- **계층적 네비게이션**: 뒤로가기 버튼 제공
- **브레드크럼**: 깊이 2단계 이상 시 경로 표시
- **메인 복귀**: 모든 화면에서 홈 버튼 제공
### 전환 효과
- **페이지 전환**: 부드러운 슬라이드 애니메이션 (300ms)
- **모달/팝업**: 페이드 인/아웃 효과 (200ms)
- **로딩 상태**: 스켈레톤 UI 또는 스피너
### URL 구조
```
/ → 로그인 페이지
/main → 메인 화면
/bill/menu → 요금조회 메뉴
/bill/result → 요금조회 결과
/product/menu → 상품변경 메뉴
/product/change → 상품변경 화면
/product/request → 상품변경 요청
/product/result → 처리결과
```
---
## 반응형 설계 전략
### 브레이크포인트
- **Mobile**: ~ 767px
- **Tablet**: 768px ~ 1023px
- **Desktop**: 1024px ~
### 레이아웃 전략
**Mobile First 설계**
- 기본: 단일 컬럼 레이아웃
- 카드 형태의 콘텐츠 구성
- 터치 친화적 버튼 크기 (44px 이상)
**Tablet**
- 2컬럼 레이아웃 (카드 그리드)
- 사이드바 네비게이션 고려
- 확장된 터치 영역
**Desktop**
- 3컬럼 레이아웃 가능
- 고정 폭 컨테이너 (최대 1200px)
- 호버 상태 적극 활용
### 콘텐츠 우선순위
1. **핵심 정보**: 항상 우선 표시
2. **액션 버튼**: 고정 위치 (하단)
3. **부가 정보**: 접기/펼치기로 제어
---
## 접근성 보장 방안
### WCAG 2.1 AA 수준 준수
**인식 가능성 (Perceivable)**
- 명도 대비 4.5:1 이상 유지
- 대체 텍스트 제공 (모든 이미지)
- 텍스트 크기 조절 가능 (최대 200%)
**운용 가능성 (Operable)**
- 키보드 접근성 완전 지원
- 포커스 순서 논리적 구성
- 자동 재생 콘텐츠 없음
**이해 가능성 (Understandable)**
- 명확한 언어 사용
- 입력 오류 방지 및 수정 지원
- 일관된 네비게이션
**견고성 (Robust)**
- 시맨틱 HTML 사용
- ARIA 라벨 적절히 활용
- 스크린 리더 호환성
### 구체적 구현 사항
- **폼 요소**: 라벨과 입력 필드 명확한 연결
- **버튼**: 명확한 텍스트 또는 aria-label
- **오류 메시지**: 명확한 위치와 해결 방법 안내
- **로딩 상태**: aria-live를 통한 상태 알림
---
## 성능 최적화 방안
### 로딩 성능
**초기 로딩**
- Critical CSS 인라인 처리
- 이미지 지연 로딩 (Lazy Loading)
- 폰트 최적화 (font-display: swap)
**코드 분할**
- 페이지별 번들 분리
- 동적 import 활용
- 트리 쉐이킹 적용
### 런타임 성능
**상태 관리**
- 불필요한 리렌더링 방지
- 메모이제이션 활용
- 가상화 (긴 목록)
**네트워크 최적화**
- API 응답 캐싱
- 요청 중복 제거
- 압축 및 minify
### 사용자 경험
**로딩 상태**
- 스켈레톤 UI 제공
- 프로그레스바 표시
- 오프라인 상태 대응
**오류 처리**
- 명확한 오류 메시지
- 재시도 메커니즘
- 폴백 UI 제공
### 성능 지표 목표
- **First Contentful Paint**: < 1.5초
- **Largest Contentful Paint**: < 2.5초
- **First Input Delay**: < 100ms
- **Cumulative Layout Shift**: < 0.1
---
## 변경 이력
| 버전 | 날짜 | 변경사항 | 작성자 |
|------|------|----------|--------|
| 1.0 | 2025-01-05 | 초기 UI/UX 설계서 작성 | 박화면 |
---
## 검토 사항
### 유저스토리 매칭 검토 ✅
- 총 10개 유저스토리 100% 반영
- 화면별 관련 유저스토리 명시
- 불필요한 추가 설계 없음
### 설계 원칙 준수 ✅
- 통신요금 관리 서비스 특화 설계
- 보안성과 사용성 균형
- 접근성 및 성능 고려