Initial commit: 신라대학교 AMP 유저관리버전

This commit is contained in:
CPABONG 2025-08-03 10:28:43 +09:00
commit 1693501d1c
479 changed files with 36666 additions and 0 deletions

16
.cursorrules Normal file
View File

@ -0,0 +1,16 @@
# Cursor AI 설정 규칙
## 터미널 명령어 자동 실행
- 사용자가 conda activate, python manage.py, pip install 등의 터미널 명령어를 요청하면 자동으로 실행
- 별도 확인 없이 즉시 명령어 실행
- Django 프로젝트 관련 명령어 (makemigrations, migrate, runserver 등) 자동 실행
## 프로젝트 컨텍스트
- Django 웹 애플리케이션 (SillaAMP_user)
- Python 3.8 환경 사용
- 현재 작업 디렉토리: C:\Users\CPABONG\Desktop\SillaAMP_user
## 코딩 스타일
- Python/Django 코드 작성 시 PEP 8 준수
- 한글 주석 사용 가능
- 명확하고 읽기 쉬운 코드 작성

0
A_core/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
A_core/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for A_core project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'A_core.settings')
application = get_asgi_application()

172
A_core/settings.py Normal file
View File

@ -0,0 +1,172 @@
"""
Django settings for A_core project.
Generated by 'django-admin startproject' using Django 4.2.16.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-kst@+h&50%!m$(d!l*qbb0l7f@z#@#me__yye^$5kg%0m%1=im'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
# Django 기본 앱들
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 필수: django.contrib.sites
'django.contrib.sites',
# allauth 관련 앱들 (순서 중요)
'allauth',
'allauth.account',
'allauth.socialaccount',
# 소셜 로그인 제공자 (필요한 경우 추가)
# 'allauth.socialaccount.providers.google',
# 프로젝트 앱들
'B_main',
'C_accounts',
]
SITE_ID = 1
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'allauth.account.middleware.AccountMiddleware',
'C_accounts.middleware.ForcePasswordSetMiddleware', # 강제 비밀번호 설정 미들웨어
]
ROOT_URLCONF = 'A_core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # 이 줄이 반드시 있어야 함
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'A_core.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
STATIC_URL = '/static/'
# 로그인/회원가입 redirect 설정
LOGIN_REDIRECT_URL = '/'
ACCOUNT_LOGOUT_REDIRECT_URL = '/accounts/login/'
# 전화번호로 로그인 (username 사용)
ACCOUNT_AUTHENTICATION_METHOD = 'username' # 'email' → 'username'
ACCOUNT_USERNAME_REQUIRED = True # username 필드 사용 (전화번호)
ACCOUNT_EMAIL_REQUIRED = False # email 필수 아님
ACCOUNT_USER_MODEL_USERNAME_FIELD = 'username' # 사용자 모델의 username 필드 활성화

29
A_core/urls.py Normal file
View File

@ -0,0 +1,29 @@
"""
URL configuration for A_core project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.views.generic import RedirectView
urlpatterns = [
path('admin/', admin.site.urls),
# allauth 비밀번호 재설정을 커스텀 시스템으로 리다이렉트
path('accounts/password/reset/', RedirectView.as_view(url='/accounts/password_reset/', permanent=False), name='account_reset_password'),
path('accounts/', include('allauth.urls')), # allauth 기본 URL
path('accounts/', include('C_accounts.urls')), # 커스텀 계정 URL
path('', include('B_main.urls')),
]

16
A_core/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for A_core project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'A_core.settings')
application = get_wsgi_application()

0
B_main/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

84
B_main/admin.py Normal file
View File

@ -0,0 +1,84 @@
from django.contrib import admin
from django.utils.html import format_html
from django import forms
from .models import Person
class PersonAdminForm(forms.ModelForm):
class Meta:
model = Person
fields = '__all__'
widgets = {
'사진': forms.FileInput(attrs={
'style': 'border: 1px solid #ccc; padding: 5px; border-radius: 3px;'
})
}
@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
form = PersonAdminForm
list_display = ['SEQUENCE', '이름', '소속', '직책', '연락처', 'user', '모든사람보기권한', '비밀번호설정필요', '사진']
list_filter = ['모든사람보기권한', '비밀번호설정필요', '소속', '직책']
search_fields = ['이름', '소속', '직책', '연락처', 'keyword1']
readonly_fields = ['수정일시', '사진미리보기']
list_editable = ['SEQUENCE']
list_display_links = ['이름']
ordering = ['이름']
fieldsets = (
('기본 정보', {
'fields': ('이름', '연락처', 'user')
}),
('상세 정보', {
'fields': ('소속', '직책', '주소', '생년월일')
}),
('미디어', {
'fields': ('사진', '사진미리보기')
}),
('설정', {
'fields': ('모든사람보기권한', '비밀번호설정필요', 'TITLE', 'SEQUENCE', 'keyword1')
}),
)
class Media:
css = {
'all': ('admin/css/custom_admin.css',)
}
def 사진미리보기(self, obj):
if obj.사진:
return format_html(
'<img src="{}" style="max-width: 100px; max-height: 100px; border-radius: 5px;" />',
obj.사진.url
)
return "사진 없음"
사진미리보기.short_description = '사진 미리보기'
def 모든사람보기권한(self, obj):
if obj.모든사람보기권한:
return format_html('<span style="color: green;">✓ 모든 사람 보기</span>')
else:
return format_html('<span style="color: blue;">👤 회원가입자만 보기</span>')
모든사람보기권한.short_description = '보기 권한'
def 비밀번호설정필요(self, obj):
if obj.비밀번호설정필요:
return format_html('<span style="color: red;">⚠️ 비밀번호 설정 필요</span>')
else:
return format_html('<span style="color: green;">✓ 비밀번호 설정 완료</span>')
비밀번호설정필요.short_description = '비밀번호 설정 상태'
def 수정일시(self, obj):
return obj.user.date_joined if obj.user else 'N/A'
수정일시.short_description = '수정일시'
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser
def has_add_permission(self, request):
return request.user.is_superuser
def has_change_permission(self, request, obj=None):
return request.user.is_superuser
def has_view_permission(self, request, obj=None):
return request.user.is_superuser

6
B_main/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class BMainConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'B_main'

102
B_main/clean_duplicates.py Normal file
View File

@ -0,0 +1,102 @@
#!/usr/bin/env python
"""
중복된 Person 데이터를 정리하는 스크립트
"""
import os
import sys
import django
# Django 설정
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'A_core.settings')
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
django.setup()
from B_main.models import Person
from django.contrib.auth.models import User
def clean_duplicates():
"""중복된 Person 데이터 정리"""
print("=" * 60)
print("중복 Person 데이터 정리")
print("=" * 60)
# 전화번호별로 그룹화
phone_groups = {}
for person in Person.objects.all():
if person.연락처:
if person.연락처 not in phone_groups:
phone_groups[person.연락처] = []
phone_groups[person.연락처].append(person)
deleted_count = 0
for phone, persons in phone_groups.items():
if len(persons) > 1:
print(f"\n전화번호 {phone}에 대한 중복 발견:")
# 회원가입 상태별로 분류
registered = [p for p in persons if p.회원가입상태 == '회원가입']
not_registered = [p for p in persons if p.회원가입상태 == '미가입']
withdrawn = [p for p in persons if p.회원가입상태 == '탈퇴']
print(f" 회원가입: {len(registered)}")
print(f" 미가입: {len(not_registered)}")
print(f" 탈퇴: {len(withdrawn)}")
# 정리 로직
if registered:
# 회원가입된 것이 있으면 나머지 삭제
keep_person = registered[0]
to_delete = persons[1:] # 첫 번째 것 제외하고 모두 삭제
print(f" 유지: {keep_person.이름} (ID: {keep_person.id}, 회원가입상태: {keep_person.회원가입상태})")
for person in to_delete:
print(f" 삭제: {person.이름} (ID: {person.id}, 회원가입상태: {person.회원가입상태})")
person.delete()
deleted_count += 1
elif not_registered:
# 미가입만 있으면 첫 번째 것만 유지
keep_person = not_registered[0]
to_delete = not_registered[1:] + withdrawn
print(f" 유지: {keep_person.이름} (ID: {keep_person.id}, 회원가입상태: {keep_person.회원가입상태})")
for person in to_delete:
print(f" 삭제: {person.이름} (ID: {person.id}, 회원가입상태: {person.회원가입상태})")
person.delete()
deleted_count += 1
elif withdrawn:
# 탈퇴만 있으면 첫 번째 것만 유지
keep_person = withdrawn[0]
to_delete = withdrawn[1:]
print(f" 유지: {keep_person.이름} (ID: {keep_person.id}, 회원가입상태: {keep_person.회원가입상태})")
for person in to_delete:
print(f" 삭제: {person.이름} (ID: {person.id}, 회원가입상태: {person.회원가입상태})")
person.delete()
deleted_count += 1
print(f"\n{deleted_count}개의 중복 데이터가 삭제되었습니다.")
# 최종 확인
print("\n최종 중복 확인:")
final_phone_counts = {}
for person in Person.objects.all():
if person.연락처:
final_phone_counts[person.연락처] = final_phone_counts.get(person.연락처, 0) + 1
final_duplicates = {phone: count for phone, count in final_phone_counts.items() if count > 1}
if final_duplicates:
print("여전히 중복된 전화번호가 있습니다:")
for phone, count in final_duplicates.items():
print(f" {phone}: {count}")
else:
print("모든 중복이 해결되었습니다.")
if __name__ == '__main__':
clean_duplicates()

297
B_main/forms.py Normal file
View File

@ -0,0 +1,297 @@
import re
from django import forms
from django.contrib.auth.models import User
from .models import Person
def format_phone_number(phone):
"""전화번호에서 대시 제거"""
return re.sub(r'[^0-9]', '', phone)
def format_phone_with_dash(phone):
"""전화번호에 대시 추가 (010-1234-5678 형식)"""
phone = format_phone_number(phone)
if len(phone) == 11:
return f"{phone[:3]}-{phone[3:7]}-{phone[7:]}"
return phone
# 허가된 사람들의 정보 (실제 데이터로 교체 필요)
PEOPLE = [
{'이름': '김봉수', '연락처': '01033433319'},
# ... 더 많은 사람들
]
def is_allowed_person(name, phone):
"""허가된 사람인지 확인"""
formatted_phone = format_phone_number(phone)
for person in PEOPLE:
if person['이름'] == name and person['연락처'] == formatted_phone:
return True
return False
def is_already_registered(name, phone):
"""이미 가입한 사용자인지 확인"""
# 전화번호 포맷팅 적용
formatted_phone = format_phone_number(phone)
# 전화번호로 User 검색 (username이 전화번호이므로)
existing_user = User.objects.filter(username=formatted_phone).first()
if existing_user:
# 해당 User와 연결된 Person이 있는 경우 (이미 회원가입한 상태)
try:
person = Person.objects.get(user=existing_user)
return True
except Person.DoesNotExist:
pass
# 이름과 전화번호로 Person 검색 (user가 있는 경우만 - 이미 회원가입한 상태)
if Person.objects.filter(
이름=name,
연락처=formatted_phone,
user__isnull=False
).exists():
return True
return False
def get_allowed_names():
"""허가된 모든 이름 목록 반환"""
return [person['이름'] for person in PEOPLE]
def get_phone_by_name(name):
"""이름으로 전화번호 찾기"""
for person in PEOPLE:
if person['이름'] == name:
return person['연락처']
return None
class CustomFileInput(forms.FileInput):
""""Currently:" 텍스트를 제거하는 커스텀 파일 입력 위젯"""
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
# "Currently:" 텍스트 제거
if 'help_text' in context:
context['help_text'] = ''
return context
class Step1PhoneForm(forms.Form):
name = forms.CharField(
max_length=50,
label='이름',
widget=forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '이름'
})
)
phone = forms.CharField(
max_length=11,
label='전화번호',
widget=forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '01012345678'
})
)
verification_code = forms.CharField(
max_length=6,
label='인증번호',
required=False,
widget=forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '6자리 인증번호'
})
)
def clean(self):
cleaned_data = super().clean()
name = cleaned_data.get('name')
phone = cleaned_data.get('phone')
if name and phone:
# 전화번호 포맷팅 적용 (대시 제거)
formatted_phone = format_phone_number(phone)
cleaned_data['phone'] = formatted_phone
# 이미 가입한 사용자인지 먼저 확인
if is_already_registered(name, formatted_phone):
raise forms.ValidationError('이미 가입한 유저입니다')
# 허가되지 않은 사용자인지 확인
if not is_allowed_person(name, formatted_phone):
raise forms.ValidationError('초대되지 않은 사용자입니다')
return cleaned_data
class Step2AccountForm(forms.Form):
password1 = forms.CharField(
label='Password',
widget=forms.PasswordInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': 'Password'
})
)
password2 = forms.CharField(
label='Password (again)',
widget=forms.PasswordInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': 'Password (again)'
})
)
privacy_agreement = forms.BooleanField(
required=True,
label='정보공개 동의',
widget=forms.CheckboxInput(attrs={
'class': 'w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2'
})
)
def clean(self):
cleaned_data = super().clean()
password1 = cleaned_data.get('password1')
password2 = cleaned_data.get('password2')
if password1 and password2 and password1 != password2:
raise forms.ValidationError('비밀번호가 일치하지 않습니다.')
return cleaned_data
def save(self, name, phone, request, commit=True):
# 전화번호 포맷팅 적용
formatted_phone = format_phone_number(phone)
# 기존 사용자가 있는지 확인 (전화번호로)
existing_user = User.objects.filter(username=formatted_phone).first()
if existing_user:
# 해당 User와 연결된 Person이 있는 경우 (이미 회원가입한 상태)
try:
person = Person.objects.get(user=existing_user)
print(f"[DEBUG] 기존 회원가입 사용자 발견: {existing_user.username}")
return existing_user
except Person.DoesNotExist:
pass
try:
# 새 사용자 생성 (전화번호를 username으로 사용)
user = User.objects.create_user(
username=formatted_phone,
email='', # 이메일은 빈 값으로 설정
password=self.cleaned_data['password1'],
first_name=name
)
# 기존 Person 정보가 있는지 확인 (user가 없는 상태)
# 전화번호는 대시 제거하여 비교
existing_person = Person.objects.filter(
이름=name,
user__isnull=True
).first()
# 전화번호 비교 (대시 제거하여)
if existing_person:
person_phone_clean = re.sub(r'[^0-9]', '', existing_person.연락처)
if person_phone_clean != formatted_phone:
existing_person = None
if existing_person:
# 기존 미가입 Person이 있으면 user 연결
existing_person.user = user
existing_person.save()
print(f"[DEBUG] 기존 Person 업데이트: {name} (user 연결)")
return user
else:
# 기존 Person이 없으면 새로 생성
Person.objects.create(
user=user,
이름=name,
연락처=format_phone_with_dash(formatted_phone), # 대시 있는 전화번호로 저장
소속='',
직책='',
주소='',
사진='profile_photos/default_user.png'
)
print(f"[DEBUG] 새 Person 생성: {name}")
return user
except Exception as e:
print(f"[DEBUG] 사용자 생성 중 오류: {e}")
# 이미 존재하는 사용자인 경우 기존 사용자 반환
existing_user = User.objects.filter(username=formatted_phone).first()
if existing_user:
return existing_user
raise e
class PhoneVerificationForm(forms.Form):
"""전화번호 인증 폼"""
name = forms.ChoiceField(
choices=[('', '이름을 선택하세요')] + [(name, name) for name in get_allowed_names()],
label='이름',
widget=forms.Select(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition'
})
)
phone = forms.CharField(
max_length=11,
label='전화번호',
widget=forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '01012345678'
})
)
verification_code = forms.CharField(
max_length=6,
label='인증번호',
widget=forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '6자리 인증번호'
})
)
def clean(self):
cleaned_data = super().clean()
name = cleaned_data.get('name')
phone = cleaned_data.get('phone')
if name and phone:
# 전화번호 포맷팅 적용 (대시 제거)
formatted_phone = format_phone_number(phone)
cleaned_data['phone'] = formatted_phone
if not is_allowed_person(name, formatted_phone):
raise forms.ValidationError('이름과 전화번호가 일치하지 않습니다.')
return cleaned_data
class PersonForm(forms.ModelForm):
"""Person 모델 폼"""
class Meta:
model = Person
fields = ['이름', '소속', '직책', '연락처', '주소', '생년월일', '사진', 'keyword1']
widgets = {
'이름': forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-200 bg-opacity-80 text-gray-500 border border-gray-300 focus:outline-none',
'readonly': True,
'tabindex': '-1',
}),
'소속': forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition'
}),
'직책': forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition'
}),
'연락처': forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-200 bg-opacity-80 text-gray-500 border border-gray-300 focus:outline-none',
'readonly': True,
'tabindex': '-1',
}),
'주소': forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition'
}),
'생년월일': forms.DateInput(attrs={
'type': 'date',
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition'
}),
'사진': CustomFileInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition'
}),
'keyword1': forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '검색 키워드 (예: 회계감사)'
}),
}

174
B_main/manual_populate.py Normal file
View File

@ -0,0 +1,174 @@
#!/usr/bin/env python
"""
수동으로 Person 데이터를 초기화하고 peopleinfo.py의 데이터로 채우는 스크립트
사용법:
python manage.py shell
exec(open('B_main/manual_populate.py').read())
"""
import os
import sys
import django
from datetime import datetime
# Django 설정
import sys
import os
# 프로젝트 루트 경로를 Python 경로에 추가
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, project_root)
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'A_core.settings')
django.setup()
from django.contrib.auth.models import User
from B_main.models import Person
from B_main.peopleinfo import PEOPLE
def clear_existing_persons():
"""superuser를 제외한 모든 Person을 삭제"""
print("기존 Person 데이터 정리 중...")
# superuser들 찾기
superusers = User.objects.filter(is_superuser=True)
if superusers.exists():
print(f"Superuser 발견: {superusers.count()}")
# 모든 superuser의 Person들을 보존
preserved_persons = []
for superuser in superusers:
person = Person.objects.filter(user=superuser).first()
if person:
preserved_persons.append(person)
print(f"보존할 Person: {person.이름} (ID: {superuser.id}, 이메일: {superuser.email})")
else:
print(f"Superuser {superuser.username} (ID: {superuser.id}, 이메일: {superuser.email}) - Person 없음")
# 모든 Person 삭제 (superuser들 제외)
deleted_count = Person.objects.exclude(user__in=superusers).delete()[0]
print(f"삭제된 Person 수: {deleted_count}")
# 보존된 Person 중 첫 번째를 반환 (또는 None)
return preserved_persons[0] if preserved_persons else None
else:
print("Superuser를 찾을 수 없습니다.")
# 모든 Person 삭제
deleted_count = Person.objects.all().delete()[0]
print(f"삭제된 Person 수: {deleted_count}")
return None
def parse_birth_date(birth_str):
"""생년월일 문자열을 Date 객체로 변환"""
if not birth_str or birth_str == '':
return None
try:
# "1960.03.27" 형식을 파싱
if '.' in birth_str:
return datetime.strptime(birth_str, '%Y.%m.%d').date()
# "1962" 형식도 처리
elif len(birth_str) == 4:
return datetime.strptime(f"{birth_str}.01.01", '%Y.%m.%d').date()
else:
return None
except ValueError:
print(f"생년월일 파싱 오류: {birth_str}")
return None
def create_persons_from_peopleinfo():
"""peopleinfo.py의 데이터로 Person 객체 생성"""
print("peopleinfo.py 데이터로 Person 생성 중...")
created_count = 0
error_count = 0
for person_data in PEOPLE:
try:
# 기본 필드들
name = person_data.get('이름', '')
affiliation = person_data.get('소속', '')
birth_date = parse_birth_date(person_data.get('생년월일', ''))
position = person_data.get('직책', '')
phone = person_data.get('연락처', '')
address = person_data.get('주소', '')
# 사진 경로에서 'media/' 접두사 제거
photo = person_data.get('사진', 'profile_photos/default_user.png')
if photo.startswith('media/'):
photo = photo[6:] # 'media/' 제거
title = person_data.get('TITLE', '')
sequence = person_data.get('SEQUENCE', None)
# SEQUENCE를 정수로 변환
if sequence and sequence != '':
try:
sequence = int(sequence)
except ValueError:
sequence = None
else:
sequence = None
# 이미 존재하는지 확인
existing_person = Person.objects.filter(이름=name, 연락처=phone).first()
if existing_person:
print(f"이미 존재하는 Person: {name} ({phone})")
continue
# 김봉수, 김태형만 보이게 설정, 나머지는 안보이게 설정
show_in_main = name in ['김봉수', '김태형']
# 새 Person 생성
person = Person.objects.create(
이름=name,
소속=affiliation,
생년월일=birth_date,
직책=position,
연락처=phone,
주소=address,
사진=photo,
TITLE=title,
SEQUENCE=sequence,
보일지여부=show_in_main
)
created_count += 1
print(f"생성됨: {name} ({phone})")
except Exception as e:
error_count += 1
print(f"오류 발생 ({name}): {str(e)}")
continue
print(f"\n생성 완료: {created_count}")
print(f"오류 발생: {error_count}")
return created_count
def main():
"""메인 실행 함수"""
print("=" * 50)
print("Person 데이터 초기화 및 재생성")
print("=" * 50)
# 1. 기존 데이터 정리
preserved_person = clear_existing_persons()
# 2. peopleinfo.py 데이터로 새로 생성
created_count = create_persons_from_peopleinfo()
# 3. 결과 요약
total_persons = Person.objects.count()
print("\n" + "=" * 50)
print("작업 완료!")
print(f"총 Person 수: {total_persons}")
if preserved_person:
print(f"보존된 Person: {preserved_person.이름} (Superuser)")
print("=" * 50)
if __name__ == "__main__":
# 직접 실행
main()

View File

@ -0,0 +1,29 @@
# Generated by Django 4.2.16 on 2025-07-31 10:15
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Person',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('이름', models.CharField(max_length=50)),
('소속', models.CharField(max_length=100)),
('생년월일', models.DateField(blank=True, null=True)),
('직책', models.CharField(max_length=50)),
('연락처', models.CharField(max_length=20)),
('주소', models.CharField(max_length=255)),
('사진', models.CharField(max_length=255)),
('TITLE', models.CharField(blank=True, max_length=50, null=True)),
('SEQUENCE', models.IntegerField(blank=True, null=True)),
],
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 4.2.16 on 2025-07-31 10:26
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('B_main', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='person',
name='user',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.16 on 2025-08-01 07:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('B_main', '0002_person_user'),
]
operations = [
migrations.AddField(
model_name='person',
name='보일지여부',
field=models.BooleanField(default=True, verbose_name='메인페이지 표시'),
),
migrations.AlterField(
model_name='person',
name='사진',
field=models.ImageField(blank=True, default='static/B_main/images/default_user.png', upload_to='profile_photos/'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.16 on 2025-08-01 11:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('B_main', '0003_person_보일지여부_alter_person_사진'),
]
operations = [
migrations.AddField(
model_name='person',
name='회원가입상태',
field=models.CharField(choices=[('미가입', '미가입'), ('회원가입', '회원가입'), ('탈퇴', '탈퇴')], default='미가입', max_length=10, verbose_name='회원가입 상태'),
),
migrations.AlterField(
model_name='person',
name='사진',
field=models.ImageField(blank=True, default='profile_photos/default_user.png', upload_to='profile_photos/'),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 4.2.16 on 2025-08-01 12:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('B_main', '0004_person_회원가입상태_alter_person_사진'),
]
operations = [
migrations.AddField(
model_name='person',
name='keyword1',
field=models.CharField(blank=True, help_text='첫 번째 키워드를 입력하세요 (예: 회계감사)', max_length=50, null=True, verbose_name='키워드1'),
),
migrations.AddField(
model_name='person',
name='keyword2',
field=models.CharField(blank=True, help_text='두 번째 키워드를 입력하세요 (예: 잡자재)', max_length=50, null=True, verbose_name='키워드2'),
),
migrations.AddField(
model_name='person',
name='keyword3',
field=models.CharField(blank=True, help_text='세 번째 키워드를 입력하세요 (예: 기획)', max_length=50, null=True, verbose_name='키워드3'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2025-08-02 13:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('B_main', '0005_person_keyword1_person_keyword2_person_keyword3'),
]
operations = [
migrations.AddField(
model_name='person',
name='모든사람보기권한',
field=models.BooleanField(default=False, help_text='True인 경우 모든 사람을 볼 수 있고, False인 경우 회원가입한 사람만 볼 수 있습니다.', verbose_name='모든 사람 보기 권한'),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.16 on 2025-08-02 15:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('B_main', '0006_person_모든사람보기권한'),
]
operations = [
migrations.RemoveField(
model_name='person',
name='보일지여부',
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.16 on 2025-08-02 15:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('B_main', '0007_remove_person_보일지여부'),
]
operations = [
migrations.RemoveField(
model_name='person',
name='회원가입상태',
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 4.2.16 on 2025-08-02 15:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('B_main', '0008_remove_person_회원가입상태'),
]
operations = [
migrations.RemoveField(
model_name='person',
name='keyword2',
),
migrations.RemoveField(
model_name='person',
name='keyword3',
),
migrations.AlterField(
model_name='person',
name='keyword1',
field=models.CharField(blank=True, help_text='다른 사람들이 당신을 찾을 수 있도록 키워드를 입력하세요 (예: 회계감사)', max_length=50, null=True, verbose_name='검색 키워드'),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 4.2.16 on 2025-08-02 16:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('B_main', '0009_remove_person_keyword2_remove_person_keyword3_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='person',
options={'verbose_name': '사람', 'verbose_name_plural': '사람들'},
),
migrations.AddField(
model_name='person',
name='비밀번호설정필요',
field=models.BooleanField(default=False, help_text='True인 경우 사용자가 메인페이지 접근 시 비밀번호 설정 페이지로 리다이렉트됩니다.', verbose_name='비밀번호 설정 필요'),
),
]

View File

Binary file not shown.

32
B_main/models.py Normal file
View File

@ -0,0 +1,32 @@
import os
from django.utils.deconstruct import deconstructible
from django.db import models
from django.contrib.auth.models import User
@deconstructible
class StaticImagePath(object):
def __call__(self, instance, filename):
# B_main 앱 폴더 하위의 static/B_main/images/에 저장
return f'B_main/static/B_main/images/{filename}'
class Person(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, null=True, blank=True)
이름 = models.CharField(max_length=50)
소속 = models.CharField(max_length=100)
생년월일 = models.DateField(null=True, blank=True)
직책 = models.CharField(max_length=50)
연락처 = models.CharField(max_length=20)
주소 = models.CharField(max_length=255)
사진 = models.ImageField(upload_to='profile_photos/', default='profile_photos/default_user.png', blank=True)
TITLE = models.CharField(max_length=50, blank=True, null=True)
SEQUENCE = models.IntegerField(blank=True, null=True)
모든사람보기권한 = models.BooleanField(default=False, verbose_name='모든 사람 보기 권한', help_text='True인 경우 모든 사람을 볼 수 있고, False인 경우 회원가입한 사람만 볼 수 있습니다.')
keyword1 = models.CharField(max_length=50, blank=True, null=True, verbose_name='검색 키워드', help_text='다른 사람들이 당신을 찾을 수 있도록 키워드를 입력하세요 (예: 회계감사)')
비밀번호설정필요 = models.BooleanField(default=False, verbose_name='비밀번호 설정 필요', help_text='True인 경우 사용자가 메인페이지 접근 시 비밀번호 설정 페이지로 리다이렉트됩니다.')
class Meta:
verbose_name = '사람'
verbose_name_plural = '사람들'
def __str__(self):
return self.이름

162
B_main/peopleinfo.py Normal file
View File

@ -0,0 +1,162 @@
PEOPLE = [
{'이름' : '강경옥','소속' : '소니아 리키엘','생년월일' : '1960.03.27','직책' : '점장','연락처' : '010-3858-5270','주소' : '부산시 기장군 기장읍 기장해안로 147 (롯데동부산 1층)','사진' : 'profile_photos/강경옥.png','TITLE':'경조국','SEQUENCE':'',},
{'이름' : '강규호','소속' : '건우건축사사무소','생년월일' : '1972.08.05','직책' : '대표','연락처' : '010-3156-9448','주소' : '부산시 진구 동평로 350(양정현대프라자, 2층, 213호)','사진' : 'profile_photos/강규호.png',},
{'이름' : '강승구','소속' : '㈜ 대원석재','생년월일' : '1971.08.09','직책' : '대표','연락처' : '010-3846-0812','주소' : '부산광역시 연제구 월드컵대로 32번길 9 ','사진' : 'profile_photos/강승구.png',},
{'이름' : '강지훈','소속' : '제이에이치툴링','생년월일' : '1989.08.22','직책' : '대표','연락처' : '010-7752-2731','주소' : '부산광역시 사상구 감전천로 252','사진' : 'profile_photos/강지훈.png',},
{'이름' : '고현숙','소속' : '의료법인 좋은사람들 ','생년월일' : '1961.08.22','직책' : '이사','연락처' : '010-3591-9400','주소' : '부산 연제구 과정로 128','사진' : 'profile_photos/고현숙.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '공한수','소속' : '부산시 서구','생년월일' : '1959.09.23','직책' : '구청장','연락처' : '010-2020-2982','주소' : '부산광역시 서구 구덕로 120','사진' : 'profile_photos/공한수.png',},
{'이름' : '곽기융','소속' : '㈜동천산업','생년월일' : '1970.09.26','직책' : '대표','연락처' : '010-3882-8394','주소' : '부산광역시 동래구 온천천로471번가길 18-3 ㈜동천산업 2층','사진' : 'profile_photos/곽기융.png',},
{'이름' : '권중천','소속' : '희창물산㈜','생년월일' : '1945.04.26','직책' : '회장','연락처' : '010-5109-2755','주소' : '부산광역시 서구 충무대로146, 희창물산㈜','사진' : 'profile_photos/권중천.png','TITLE':'고문회장','SEQUENCE':'6',},
{'이름' : '김가현','소속' : '스카이블루에셋㈜','생년월일' : '1973.06.16','직책' : '팀장','연락처' : '010-4544-7379','주소' : '부산시 동구 조방로 14, 동일타워 10층 위너스 지점','사진' : 'profile_photos/김가현.png',},
{'이름' : '김기재','소속' : '부산시 영도구','생년월일' : '1957.05.29','직책' : '구청장','연락처' : '010-3867-3368','주소' : '','사진' : 'profile_photos/김기재.png',},
{'이름' : '김기호','소속' : '경동개발','생년월일' : '1966.01.05','직책' : '대표','연락처' : '010-3131-9092','주소' : '경남 양산시 동면 금오 12길 83','사진' : 'profile_photos/김기호.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '김대성','소속' : '㈜지에스어패럴 ','생년월일' : '1974.11.05','직책' : '대표','연락처' : '010-4877-4277','주소' : '부산 남구 동제당로 12 3층,4층','사진' : 'profile_photos/김대성.png','TITLE':'기획사무국','SEQUENCE':'',},
{'이름' : '김동화','소속' : 'KDB생명','생년월일' : '1966.04.18','직책' : '지점장','연락처' : '010-6677-8079','주소' : '부산시 진구 중앙대로 640 ABL건물 생명빌딩 10층','사진' : 'profile_photos/김동화.png','TITLE':'홍보국','SEQUENCE':'',},
{'이름' : '김미경','소속' : '㈜동남석면환경연구소','생년월일' : '1976.06.23','직책' : '대표','연락처' : '010-4579-4781','주소' : '부산광역시 연제구 고분로 55번길24','사진' : 'profile_photos/김미경.png',},
{'이름' : '김미애','소속' : '예재원','생년월일' : '1976.02.12','직책' : '대표','연락처' : '010-3568-8055','주소' : '부산광역시 동래구 여고북로 215-1 1층','사진' : 'profile_photos/김미애.png',},
{'이름' : '김민주','소속' : '웰킨 두피/탈모센터','생년월일' : '1969.11.20','직책' : '원장','연락처' : '010-4221-0515','주소' : '부산시 금정구 중앙대로 1629번길26 금샘빌딩 4층','사진' : 'profile_photos/김민주.png',},
{'이름' : '김보성','소속' : '가온기업','생년월일' : '1987.07.18','직책' : '대표','연락처' : '010-9328-0588','주소' : '부산광역시 사상구 낙동대로1452번길 35','사진' : 'profile_photos/김보성.png','TITLE':'기획사무국','SEQUENCE':'',},
{'이름' : '김봉수','소속' : '선민회계법인','생년월일' : '1979.01.03','직책' : '이사','연락처' : '010-3343-3319','주소' : '부산시 동구 조방로14 동일타워 413호 선민회계법인','사진' : 'profile_photos/김봉수.png','TITLE':'감사','SEQUENCE':'',},
{'이름' : '김상준','소속' : '부산지방검찰청 서부지청','생년월일' : '1979.05.11','직책' : '형사2부장검사','연락처' : '010-7373-8126','주소' : '부산 강서구 명지국제7로 67(명지동) 부산지방검찰청 서부지청','사진' : 'profile_photos/김상준.png',},
{'이름' : '김선이','소속' : '롯데백화점 세인트앤드류스','생년월일' : '1961.04.22','직책' : '대표','연락처' : '010-5391-6021','주소' : '부산진구 가야대로 772 4층','사진' : 'profile_photos/김선이.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '김성주','소속' : '㈜ 현진','생년월일' : '1955.10.05','직책' : '대표','연락처' : '010-3863-7207','주소' : '부산 강서구 생곡산단2로11번길 33','사진' : 'profile_photos/김성주.png','TITLE':'고문','SEQUENCE':'',},
{'이름' : '김성훈','소속' : 'THE SYSTEM','생년월일' : '1972.05.16','직책' : '대표','연락처' : '010-4840-1197','주소' : '부산광역시 강서구 미음국제5로마길 5, 3층','사진' : 'profile_photos/김성훈.png',},
{'이름' : '김영하','소속' : '싱싱F.S','생년월일' : '1987.06.14','직책' : '대표','연락처' : '010-2006-5106','주소' : '부산시 사상구 새벽로 131 산업용재 유통상가','사진' : 'profile_photos/김영하.png',},
{'이름' : '김영훈','소속' : '해운대비치골프앤리조트','생년월일' : '1975.03.09','직책' : '전무','연락처' : '010-8081-3345','주소' : '부산광역시 기장군 대변로 74','사진' : 'profile_photos/김영훈.png',},
{'이름' : '김외숙','소속' : '㈜주승','생년월일' : '1968.12.20','직책' : '대표','연락처' : '010-2110-1173','주소' : '부산시 사상구 삼락천로 138','사진' : 'profile_photos/김외숙.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '김용권','소속' : '㈜태성산업기계','생년월일' : '1971.08.20','직책' : '대표','연락처' : '010-2592-4402','주소' : '','사진' : 'profile_photos/김용권.png',},
{'이름' : '김윤규','소속' : '㈜ 동남엔지니어링','생년월일' : '1968.11.25','직책' : '대표','연락처' : '010-5448-0650','주소' : '부산광역시 강서구 미음산단로8번길 80','사진' : 'profile_photos/김윤규.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '김윤아','소속' : '㈜한국분석센터','생년월일' : '1964.04.18','직책' : '대표','연락처' : '010-3854-7940','주소' : '부산광역시 사상구 학감대로 133번길 13 ㈜한국분석센터','사진' : 'profile_photos/김윤아.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '김은희','소속' : '씨앤웍스 ','생년월일' : '1976.06.08','직책' : '대표','연락처' : '010-2568-6258','주소' : '부산시 해운대구 센텀서로 30 KNN타워 1506호','사진' : 'profile_photos/김은희.png',},
{'이름' : '김일곤','소속' : '다사랑 문고','생년월일' : '1966.02.01','직책' : '대표','연락처' : '010-2549-4459','주소' : '부산광역시 금정구 부산대학로 49 오션프라자 1층','사진' : 'profile_photos/김일곤.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '김재준','소속' : '㈜ 성강개발 JOB','생년월일' : '1977.01.14','직책' : '대표','연락처' : '010-8873-8282','주소' : '부산시 진구 부전동 262-15 3층','사진' : 'profile_photos/김재준.png',},
{'이름' : '김정호','소속' : '㈜아신비에스','생년월일' : '1975.10.02','직책' : '대표','연락처' : '010-8466-5106','주소' : '부산 서구 원양로 35 국제수산물도매시장 도매장동 3층 104호','사진' : 'profile_photos/김정호.png',},
{'이름' : '김준수','소속' : '법무법인 로인','생년월일' : '1989.12.25','직책' : '대표변호사','연락처' : '010-6898-0505','주소' : '부산 연제구 법원로 28 801호','사진' : 'profile_photos/김준수.png',},
{'이름' : '김중선','소속' : '연일한우참숯구이','생년월일' : '1961.11.19','직책' : '사장','연락처' : '010-2783-6974','주소' : '부산시 연제구 고분로32번길 42 1층','사진' : 'profile_photos/김중선.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '김진홍','소속' : '부산시 동구','생년월일' : '1957.11.17','직책' : '구청장','연락처' : '010-5609-5609','주소' : '','사진' : 'profile_photos/김진홍.png',},
{'이름' : '김태영','소속' : '가가산업개발㈜','생년월일' : '1972.11.25','직책' : '대표','연락처' : '010-6267-4598','주소' : '부산시 북구 금곡대로 616번길 25 3층','사진' : 'profile_photos/김태영.png',},
{'이름' : '김태형','소속' : '해우법무사사무소','생년월일' : '1962.02.27','직책' : '대표','연락처' : '010-6338-9339','주소' : '부산 연제구 법원로 34 909호(거제동, 정림빌딩)','사진' : 'profile_photos/김태형.png','TITLE':'총무국장','SEQUENCE':'',},
{'이름' : '김한집','소속' : '사상기업발전협의회','생년월일' : '1959.10.22','직책' : '회장','연락처' : '010-4646-0560','주소' : '부산시 사상구 사상로 440번길 28(모라동)','사진' : 'profile_photos/김한집.png','TITLE':'고문','SEQUENCE':'',},
{'이름' : '김현우','소속' : '부산교통공사','생년월일' : '1969.12.02','직책' : '기획예산실장','연락처' : '010-8007-9813','주소' : '부산광역시 부산진구 중앙대로 644번길 20','사진' : 'profile_photos/김현우.png',},
{'이름' : '김현준','소속' : 'BNK 부산은행','생년월일' : '1970.07.13','직책' : '상무','연락처' : '010-3590-9457','주소' : '부산광역시 남구 문현금융로 30, 부산은행 본점 18층','사진' : 'profile_photos/김현준.png',},
{'이름' : '김희경','소속' : '오케이물류 ','생년월일' : '1970.04.15','직책' : '부장','연락처' : '010-5858-3136','주소' : '부산 중구 대청로 155번길 6 오케이물류㈜','사진' : 'profile_photos/김희경(수정).png',},
{'이름' : '노현주','소속' : '이앤씨상봉㈜','생년월일' : '1960.11.27','직책' : '이사','연락처' : '010-3857-2756','주소' : '부산시 동래구 충렬대로 107번길','사진' : 'profile_photos/노현주.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '노희숙','소속' : '더베스트금융㈜부경','생년월일' : '1966.12.01','직책' : '대표','연락처' : '010-8398-5508','주소' : '부산시 동래구 온천천로 179-1 휘담채 3층','사진' : 'profile_photos/노희숙.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '마점래','소속' : '㈜ 엠오티','생년월일' : '1962.03.17','직책' : '회장','연락처' : '010-3591-1575','주소' : '경상남도 양산시 상북면 석계산단2길 46','사진' : 'profile_photos/마점래(수정).png','TITLE':'선임상임부회장','SEQUENCE':'7',},
{'이름' : '문성배','소속' : '(주)SMC ','생년월일' : '1965.05.06','직책' : '대표','연락처' : '010-9304-0388','주소' : '부산광역시 동구 수정중로 11번길 29, 2층','사진' : 'profile_photos/문성배.png','TITLE':'감사','SEQUENCE':'',},
{'이름' : '문정순','소속' : '지클랩','생년월일' : '1968.05.29','직책' : '대표','연락처' : '010-9800-4848','주소' : '경기도 화성시 동탄대로 636-1 911호','사진' : 'profile_photos/문정순.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '민수연','소속' : 'BS부산오페라단','생년월일' : '1974.04.05','직책' : '단장','연락처' : '010-8448-8358','주소' : '부산광역시 금강로 380번길 21 2층','사진' : 'profile_photos/민수연.png','TITLE':'친교문화국','SEQUENCE':'',},
{'이름' : '민홍기','소속' : '민플란트치과의원','생년월일' : '1988.06.08','직책' : '원장','연락처' : '010-8509-4470','주소' : '부산 해운대구 센텀남대로 50 A1102호, A1103호','사진' : 'profile_photos/민홍기.png',},
{'이름' : '박강범','소속' : '부영회계법인','생년월일' : '1981.10.23','직책' : '대표','연락처' : '010-3949-8866','주소' : '부산광역시 해운대구 센텀중앙로97, 센텀스카이비즈 3707호','사진' : 'profile_photos/박강범.png',},
{'이름' : '박경민','소속' : '로한종합건설㈜','생년월일' : '1976.07.31','직책' : '대표','연락처' : '010-9961-9699','주소' : '부산광역시 기장군 장안읍 고무로 129','사진' : 'profile_photos/박경민.png',},
{'이름' : '박국제','소속' : '㈜국제경영기술원','생년월일' : '1951.06.15','직책' : '원장','연락처' : '010-3842-5063','주소' : '부산광역시 금정구 두실로24번길 12','사진' : 'profile_photos/박국제.png','TITLE':'고문','SEQUENCE':'',},
{'이름' : '박대진','소속' : '한몽경영인협의회','생년월일' : '1973.05.10','직책' : '회장','연락처' : '010-2610-0531','주소' : '강남구 테헤란로 82길 15 574호','사진' : 'profile_photos/박대진.png','TITLE':'홍보국','SEQUENCE':'',},
{'이름' : '박명숙','소속' : '거산통상','생년월일' : '1968.04.08','직책' : '대표','연락처' : '010-6289-1777','주소' : '부산시 사상구 괘감로 37, 11동 107호 거산통상','사진' : 'profile_photos/박명숙.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '박명옥','소속' : '㈜참한디자인','생년월일' : '1966.03.25','직책' : '이사','연락처' : '010-8551-5871','주소' : '부산시 연제구 거제시장로 15 참한빌딩 3층','사진' : 'profile_photos/박명옥.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '박명진','소속' : '해운대비치골프앤리조트','생년월일' : '1962','직책' : '회장','연락처' : '010-6267-8188','주소' : '부산시 기장군 기장읍 대변로 74 해운대비치골프앤리조트','사진' : 'profile_photos/박명진.png','TITLE':'고문','SEQUENCE':'',},
{'이름' : '박민희','소속' : '㈜청운','생년월일' : '1989.05.18','직책' : '대표','연락처' : '010-3168-7872','주소' : '경남 양산시 동면 외송로30,701동1601호(사송더샵데시앙2차7단지)','사진' : 'profile_photos/박민희.png','TITLE':'친교문화국','SEQUENCE':'',},
{'이름' : '박부술','소속' : '(주)삼림물산','생년월일' : '1968.05.13','직책' : '대표','연락처' : '010-9880-7422','주소' : '부산시 강서구 녹간산단 407로 8','사진' : 'profile_photos/박부술.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '박성호','소속' : '부산진해경제자유구역청','생년월일' : '1966.12.13','직책' : '청장','연락처' : '010-5641-5815','주소' : '부산광역시 강서구 녹산산단232로 38-26로','사진' : 'profile_photos/박성호.png',},
{'이름' : '박성훈','소속' : '국민의힘 부산 북구(을)','생년월일' : '1971.01.18','직책' : '국회의원','연락처' : '010-6760-3435','주소' : '','사진' : 'profile_photos/박성훈.png',},
{'이름' : '박순자','소속' : '','생년월일' : '1958.01.02','직책' : '','연락처' : '010-2383-1296','주소' : '부산시 동래구 명륜로 49 센트럴 B/D 6층','사진' : 'profile_photos/박순자.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '박영우','소속' : '퍼시픽링스코리아 영남지사','생년월일' : '1969.01.29','직책' : '지사장','연락처' : '010-3844-8255','주소' : '부산시 해운대구 센텀서로 30 209호','사진' : 'profile_photos/박영우.png','TITLE':'친교문화국','SEQUENCE':'',},
{'이름' : '박영해','소속' : '건양사이버대학교','생년월일' : '1961.05.19','직책' : '교수','연락처' : '010-3542-3578','주소' : '대전시 서구 관저동로 158','사진' : 'profile_photos/박영해.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '박재성','소속' : '㈜이앤아이솔루션','생년월일' : '1989.03.02','직책' : '대표','연락처' : '010-9963-5420','주소' : '부산시 수영구 수영로 488번길 26 재안빌딩 4층','사진' : 'profile_photos/박재성.png',},
{'이름' : '박정숙','소속' : '(주)노벨홀딩스','생년월일' : '1979.01.10','직책' : '대표','연락처' : '010-8076-2001','주소' : '부산시 부산진구 가야대로 473 4층','사진' : 'profile_photos/박정숙.png',},
{'이름' : '박정은','소속' : '㈜승진','생년월일' : '1984.04.02','직책' : '대표','연락처' : '010-6484-4402','주소' : '부산광역시 강서구 미음동 1554-6','사진' : 'profile_photos/박정은.png',},
{'이름' : '박종인','소속' : '법무법인 로베리 ','생년월일' : '1976.03.15','직책' : '부산사무소 대표(파트너변호사)','연락처' : '010-5564-1791','주소' : '부산 연제구 법원남로 15번길 26 위너스빌딩 3층','사진' : 'profile_photos/박종인.png',},
{'이름' : '박주원','소속' : '㈜ 온나라 부동산 중개법인','생년월일' : '1965.10.30','직책' : '부사장','연락처' : '010-5624-5321','주소' : '부산시 연제구 중앙대로 144 우전빌딩 2층','사진' : 'profile_photos/박주원(수정).png','TITLE':'기획사무국','SEQUENCE':'',},
{'이름' : '박지환','소속' : '㈜푸르다','생년월일' : '1960.06.05','직책' : '대표','연락처' : '010-3844-7818','주소' : '부산시 기장군 일광면 체육공원1로 3','사진' : 'profile_photos/박지환.png','TITLE':'상임부회장','SEQUENCE':'',},
{'이름' : '배범한','소속' : '㈜대산컨설팅','생년월일' : '1980.10.02','직책' : '대표','연락처' : '010-2223-3940','주소' : '부산광역시 해운대구 반여로 186-7, 1층','사진' : 'profile_photos/배범한.png',},
{'이름' : '배성효','소속' : '법무법인 무한','생년월일' : '1964.11.25','직책' : '대표변호사','연락처' : '010-6201-2117','주소' : '부산 연제구 법원북로 86, 9층(거제동, 만해빌딩)','사진' : 'profile_photos/배성효.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '변연옥','소속' : '홍콩반점 양산역점 ','생년월일' : '1970.10.03','직책' : '대표','연락처' : '010-7765-7890','주소' : '양산역 3길 16 101호','사진' : 'profile_photos/변연옥.png',},
{'이름' : '빈윤진','소속' : '진무역','생년월일' : '1964.10.05','직책' : '대표','연락처' : '010-3567-2854','주소' : '부산시 남구 문현동 고동골로 10-1 동양빌딩 3층','사진' : 'profile_photos/빈윤진.png','TITLE':'상임부회장','SEQUENCE':'',},
{'이름' : '서강섭','소속' : '㈜호방종합건설','생년월일' : '1970.09.28','직책' : '대표','연락처' : '010-3856-7303','주소' : '부산시 사상구 덕상로 22, 403호(덕포동, 명성빌딩)','사진' : 'profile_photos/서강섭.png','TITLE':'경조국','SEQUENCE':'',},
{'이름' : '서지윤','소속' : '㈜리만 부산 센텀지사','생년월일' : '1972.09.20','직책' : '지사장','연락처' : '010-2823-4375','주소' : '부산시 해운대구 센텀동로9 트럼프월드센텀 209호','사진' : 'profile_photos/서지윤.png',},
{'이름' : '성동화','소속' : '부산신용보증재단','생년월일' : '1961.10.17','직책' : '이사장','연락처' : '010-8787-5902','주소' : '부산광역시 부산진구 진연로 15(양정동)','사진' : 'profile_photos/성동화.png',},
{'이름' : '성충식','소속' : '㈜에이스여행사','생년월일' : '1962.01.03','직책' : '대표','연락처' : '010-5007-9178','주소' : '부산시 중구 해관로73 일광빌딩 3층','사진' : 'profile_photos/성충식.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '손동현','소속' : '아트스윙','생년월일' : '1985.12.09','직책' : '대표','연락처' : '010-3883-7113','주소' : '부산 북구 만덕3로 16번길 1 부산이노비즈센터 206호 아트스윙','사진' : 'profile_photos/손동현.png','TITLE':'홍보국','SEQUENCE':'',},
{'이름' : '송연익','소속' : '(주)에스엠산업','생년월일' : '1968.06.26','직책' : '대표','연락처' : '010-3849-2100','주소' : '서울시 강남구 대치4동 910-6번지 202동 202호','사진' : 'profile_photos/송연익.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '심수현','소속' : '카페레빗','생년월일' : '1969.08.19','직책' : '대표','연락처' : '010-8234-8776','주소' : '해운대구 달맞이 길 62번 길 5-57 카페레빗','사진' : 'profile_photos/심수현.png',},
{'이름' : '안복지','소속' : '플로라네트(꽃사세요)','생년월일' : '1972.09.01','직책' : '대표','연락처' : '010-5499-3339','주소' : '부산시 남구 수영로 26 (대림문현시티프라자) 107호','사진' : 'profile_photos/안복지.png','TITLE':'경조국','SEQUENCE':'',},
{'이름' : '안상배','소속' : '법무법인 예주','생년월일' : '1984.03.28','직책' : '대표변호사','연락처' : '010-6683-3981','주소' : '부산광역시 연제구 법원남로15번길 10, 6층(거제동, 미르코아빌딩)','사진' : 'profile_photos/안상배.png','TITLE':'기획사무국','SEQUENCE':'',},
{'이름' : '안영봉','소속' : '남부경찰서','생년월일' : '1970.05.14','직책' : '서장','연락처' : '010-3563-8339','주소' : '부산광역시 연제구 중앙대로 999','사진' : 'profile_photos/안영봉.png',},
{'이름' : '양재진','소속' : '㈜ 한림기업','생년월일' : '1970.06.21','직책' : '대표 ','연락처' : '010-7181-3241','주소' : '부산광역시 기장군 장안읍 반룡산단1로55','사진' : 'profile_photos/양재진.png',},
{'이름' : '어익수','소속' : '㈜ 다오테크','생년월일' : '1964.07.05','직책' : '대표','연락처' : '010-4552-2495','주소' : '부산광역시 영도구 남향서로 119(남향동1가 9번지)','사진' : 'profile_photos/어익수.png','TITLE':'상임부회장','SEQUENCE':'',},
{'이름' : '엄신아','소속' : '신라횟집','생년월일' : '1971.10.12','직책' : '대표','연락처' : '010-6656-5837','주소' : '부산시 수영구 민학수변로 7번가 16','사진' : 'profile_photos/엄신아.png',},
{'이름' : '여은주','소속' : '지구산업','생년월일' : '1965.04.27','직책' : '대표','연락처' : '010-6818-2045','주소' : '부산광역시 부산진구 신천대로 71번길 23(범천동)','사진' : 'profile_photos/여은주.png','TITLE':'재무국','SEQUENCE':'',},
{'이름' : '예영숙','소속' : '삼성생명보험㈜','생년월일' : '1960.11.18','직책' : '명예전무','연락처' : '010-3532-3519','주소' : '대구광역시 중구 달구벌대로 2095,삼성생명빌딩 9층 예영숙명예전무실','사진' : 'profile_photos/예영숙.png','TITLE':'고문','SEQUENCE':'',},
{'이름' : '오용택','소속' : '한택','생년월일' : '1965.07.29','직책' : '대표','연락처' : '010-8512-3238','주소' : '경남 김해시 김해대로 2596번길125 (지내동)','사진' : 'profile_photos/오용택.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '오원재','소속' : '㈜에스씨원건설','생년월일' : '1970.11.26','직책' : '대표','연락처' : '010-2585-2424','주소' : '부산광역시 북구 금곡대로616번길 135','사진' : 'profile_photos/오원재.png','TITLE':'재무국','SEQUENCE':'',},
{'이름' : '유석찬','소속' : '서호종합건설㈜','생년월일' : '1968.02.28','직책' : '대표','연락처' : '010-9320-7007','주소' : '부산시 강서구 명지국제2로28번길 26, 602호(명지동, 퍼스트삼융)','사진' : 'profile_photos/유석찬.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '이기영','소속' : 'BNK부산은행 부전동금융센터','생년월일' : '1971.11.22','직책' : '금융센터장','연락처' : '010-3882-1516','주소' : '부산광역시 부산진구 새싹로 1(부전동)','사진' : 'profile_photos/이기영.png',},
{'이름' : '이대명','소속' : '법무사 이대명 사무소','생년월일' : '1963.08.25','직책' : '법무사','연락처' : '010-2834-8248','주소' : '부산 연제구 법원남로 16번길 21','사진' : 'profile_photos/이대명.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '이두홍','소속' : '㈜ 동남창호산업','생년월일' : '1974.05.18','직책' : '대표','연락처' : '010-2602-8263','주소' : '경남 양산시 상북면 수서로 70','사진' : 'profile_photos/이두홍.png','TITLE':'친교문화국','SEQUENCE':'',},
{'이름' : '이범민','소속' : '㈜ 와이즈','생년월일' : '1985.02.09','직책' : '대표','연락처' : '010-9334-1364','주소' : '경남 양산시 물금읍 서들8길 45, 4층 와이즈','사진' : 'profile_photos/이범민.png',},
{'이름' : '이상경','소속' : '법무법인 태종','생년월일' : '1964.12.22','직책' : '대표변호사','연락처' : '010-5351-1866','주소' : '부산 연제구 법원로 12,701호','사진' : 'profile_photos/이상경.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '이상규','소속' : '㈜보문','생년월일' : '1956.09.29','직책' : '대표','연락처' : '010-3887-1288','주소' : '부산광역시 부산진구 동천로108번길 14','사진' : 'profile_photos/이상규.png','TITLE':'고문','SEQUENCE':'',},
{'이름' : '이상민','소속' : '(주)월드이노텍','생년월일' : '1987.05.03','직책' : '전무','연락처' : '010-3230-9354','주소' : '경상남도 양산시 덕계동 웅상농공단지길 42 월드이노텍','사진' : 'profile_photos/이상민.png',},
{'이름' : '이수연','소속' : '이수연 힐링예술원','생년월일' : '1969.04.15','직책' : '원장','연락처' : '010-5539-9999','주소' : '부산시 연제구 세병로 6, 3층','사진' : 'profile_photos/이수연.png',},
{'이름' : '이순옥','소속' : '삼성생명보험㈜','생년월일' : '1963.12.17','직책' : '명예상무','연락처' : '010-3871-8088','주소' : '부산시 동구 중앙대로 222 삼성생명빌딩 황도지점','사진' : 'profile_photos/이순옥.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '이승규','소속' : '성심종합건설㈜','생년월일' : '1959.11.20','직책' : '대표','연락처' : '010-7248-3301','주소' : '부산광역시 사하구 감천로134, (감천동, 중모빌딩) 6층','사진' : 'profile_photos/이승규.png','TITLE':'회장','SEQUENCE':'4',},
{'이름' : '이영희','소속' : 'IBK투자증권','생년월일' : '1961.07.22','직책' : '자문역','연락처' : '010-4329-9432','주소' : '부산광역시 해운대구 센텀남대로 50 (우동) 임페리얼타워 2층','사진' : 'profile_photos/이영희.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '이인숙','소속' : '큐앤㈜','생년월일' : '1969.05.10','직책' : '대표','연락처' : '010-9092-0510','주소' : '부산광역시 남구 고동골로 78번길 12(문현동 창암빌딩 6층)','사진' : 'profile_photos/이인숙.png',},
{'이름' : '이정문','소속' : '글로벌산업','생년월일' : '1967.03.15','직책' : '대표','연락처' : '010-3860-3650','주소' : '부산시 사상구 새벽시장로 19-19','사진' : 'profile_photos/이정문.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '이주영','소속' : '인(IN)코칭연구소','생년월일' : '1976.05.01','직책' : '대표','연락처' : '010-4872-8837','주소' : '부산 부산진구 서전로37번길 25-9, 802호','사진' : 'profile_photos/이주영.png',},
{'이름' : '이학민','소속' : '㈜신화이엔지','생년월일' : '1983.08.03','직책' : '대표','연락처' : '010-9306-9994','주소' : '부산광역시 강서구 미읍산단로 37번길 9(구량동) 1층 주식회사 신화이엔지','사진' : 'profile_photos/이학민.png',},
{'이름' : '이향숙','소속' : '메이저','생년월일' : '1963.03.23','직책' : '대표','연락처' : '010-9312-3023','주소' : '부산시 해운대구 우동 203번지 오션타워 4층','사진' : 'profile_photos/이향숙.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '이현수','소속' : '삼우종합개발','생년월일' : '1961.06.24','직책' : '대표','연락처' : '010-3874-1222','주소' : '부산광역시 사하구 다대로 531','사진' : 'profile_photos/이현수.png','TITLE':'상임부회장','SEQUENCE':'',},
{'이름' : '이현우','소속' : '㈜은하수산 ','생년월일' : '1964.08.24','직책' : '대표','연락처' : '010-3593-7888','주소' : '부산광역시 강서구 녹산산단 381로 36`','사진' : 'profile_photos/이현우.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '이현정','소속' : '㈜ 스윗솔루션','생년월일' : '1981.11.25','직책' : '대표','연락처' : '010-5109-5789','주소' : '부산 강서구 범방3로78번길 25','사진' : 'profile_photos/이현정.png',},
{'이름' : '이화경','소속' : '해동산업㈜','생년월일' : '1965.04.03','직책' : '이사','연락처' : '010-5436-7257','주소' : '부산시 사상구 감전동 낙동대로 1052','사진' : 'profile_photos/이화경.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '임문수','소속' : '㈜휴먼스컴퍼니','생년월일' : '1963.03.22','직책' : '대표','연락처' : '010-4130-4750','주소' : '서울 서대문구 충정리시온 217호','사진' : 'profile_photos/임문수.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '임영민','소속' : '티엔에스무역㈜','생년월일' : '1988.10.29','직책' : '전무','연락처' : '010-9699-8113','주소' : '부산광역시 중구 태종로 14-1, 반도빌딩 4층, 티엔에스무역㈜','사진' : 'profile_photos/임영민.png',},
{'이름' : '임윤택','소속' : '센텀종합병원','생년월일' : '1960.01.06','직책' : '행정부장','연락처' : '010-6561-2222','주소' : '부산광역시 수영구 수영로 679번길 8 센텀종합병원 신관 14층','사진' : 'profile_photos/임윤택.png',},
{'이름' : '임창섭','소속' : '㈜ 동신','생년월일' : '1966.08.15','직책' : '회장','연락처' : '010-4128-3343','주소' : '부산광역시 해운대구 우동1로 20번길 27-10 ㈜동신','사진' : 'profile_photos/임창섭(수정).png','TITLE':'상임부회장','SEQUENCE':'',},
{'이름' : '장은화','소속' : '정덕기업경영연구원','생년월일' : '1973.04.17','직책' : '대표','연락처' : '010-8892-9635','주소' : '부산시 동구 중앙대로 409 디알라이프시티 305호','사진' : 'profile_photos/장은화.png',},
{'이름' : '장지훈','소속' : '㈜다우플랫폼','생년월일' : '1983.04.07','직책' : '대표','연락처' : '010-9459-0579','주소' : '부산시 연제구 과정로 276번가길 32, 2층','사진' : 'profile_photos/장지훈.png',},
{'이름' : '장현정','소속' : '㈜ 고앤파트너스','생년월일' : '1978.05.05','직책' : '이사','연락처' : '010-3329-1226','주소' : '창원시 의창구 용동로 83번안길 7, 401호(사림동, 미래드림빌딩)','사진' : 'profile_photos/장현정.png',},
{'이름' : '전미영','소속' : 'IBK기업은행 부산지점','생년월일' : '1971.05.23','직책' : '부지점장','연락처' : '010-5567-7514','주소' : '부산광역시 중구 중앙대로88 (중앙동4가) 기업은행 부산지점','사진' : 'profile_photos/전미영.png',},
{'이름' : '전병웅','소속' : '㈜이진주택','생년월일' : '1974.09.25','직책' : '대표','연락처' : '010-4553-6301','주소' : '부산광역시 수영구 연수로 405, 4층','사진' : 'profile_photos/전병웅(수정).png',},
{'이름' : '전성훈','소속' : '대원플러스그룹','생년월일' : '1971.05.25','직책' : '이사','연락처' : '010-2857-9157','주소' : '부산시 해운대구 마린시티2로33 제니스스퀘어 A동 402호','사진' : 'profile_photos/전성훈.png',},
{'이름' : '전종태','소속' : '㈜디엔씨텍','생년월일' : '1956.08.27','직책' : '대표','연락처' : '010-6583-3004','주소' : '경기도 화성시 장안면 석포로 94-21','사진' : 'profile_photos/전종태.png','TITLE':'고문','SEQUENCE':'',},
{'이름' : '전희충','소속' : '㈜대웅이티','생년월일' : '1962.08.01','직책' : '대 표','연락처' : '010-3001-3435','주소' : '부산광역시 강서구 화전산단3로 90 ㈜대웅이티','사진' : 'profile_photos/전희충.png','TITLE':'상임부회장','SEQUENCE':'',},
{'이름' : '정석민','소속' : '㈜연우 비즈니스 컨설팅','생년월일' : '1974.10.16','직책' : '대표','연락처' : '010-4552-0609','주소' : '부산 해운대구 해운대로 790(좌동, 대림아크로텔) 1601호','사진' : 'profile_photos/정석민.png',},
{'이름' : '정용표','소속' : '㈜케이에이엠','생년월일' : '1959.01.06','직책' : '대표','연락처' : '010-3868-4103','주소' : '부산시 강서구 녹산산단382로 50번길30','사진' : 'profile_photos/정용표.png','TITLE':'수석부회장','SEQUENCE':'5',},
{'이름' : '정윤목','소속' : '㈜금양프린텍 ','생년월일' : '1955.03.03','직책' : '대표','연락처' : '010-3863-4546','주소' : '부산광역시 중구 흑교로 35번길 9(부평동3가)','사진' : 'profile_photos/정윤목.png','TITLE':'경조국','SEQUENCE':'',},
{'이름' : '정의석','소속' : '국제식품','생년월일' : '1982.07.08','직책' : '본부장','연락처' : '010-6309-7896','주소' : '부산시 진구 거제대로 70 국제식품빌딩','사진' : 'profile_photos/정의석.png',},
{'이름' : '정종복','소속' : '부산광역시 기장군청','생년월일' : '1954.11.20','직책' : '군수','연락처' : '010-3574-8512','주소' : '부산 기장군 기장읍 기장대로 560 기장군청','사진' : 'profile_photos/정종복.png',},
{'이름' : '정형재','소속' : '㈜하우스메이커','생년월일' : '1974.10.23','직책' : '대표','연락처' : '010-6330-9911','주소' : '부산광역시 남구 동제당로 2(문현동 1층)','사진' : 'profile_photos/정형재.png',},
{'이름' : '제권진','소속' : '좋은엘리베이터㈜','생년월일' : '1972.04.12','직책' : '대표','연락처' : '010-8002-8255','주소' : '부산시 강서구 신로산단1로 101, 상가동 304호(신호동, 부산신호사랑으로부영5차)','사진' : 'profile_photos/제권진.png',},
{'이름' : '제오수','소속' : '㈜에스비안전','생년월일' : '1957.06.12','직책' : '대표','연락처' : '010-3850-5728','주소' : '부산광역시 강서구 녹산산다382로14번길 55(녹산협업화사업장)','사진' : 'profile_photos/제오수(수정).png','TITLE':'고문','SEQUENCE':'',},
{'이름' : '제일호','소속' : '경동건설㈜','생년월일' : '1975.10.25','직책' : '부장','연락처' : '010-4446-7515','주소' : '부산광역시 연제구 황령산로 599(연산동 1985-12번지)','사진' : 'profile_photos/제일호.png',},
{'이름' : '조승민','소속' : '중소벤처기업진흥공단 부산지역본부','생년월일' : '1970.04.28','직책' : '본부장','연락처' : '010-3033-9055','주소' : '부산시 부산진구 중앙대로 639번지 엠디엠타워 23층','사진' : 'profile_photos/조승민.png',},
{'이름' : '조엘리사','소속' : '㈜꼬레아티에스','생년월일' : '1973.01.01','직책' : '대표','연락처' : '010-6421-3240','주소' : '부산 사상구 광장로 56번길 56(3층, 괘법동)','사진' : 'profile_photos/조엘리사.png','TITLE':'홍보국','SEQUENCE':'',},
{'이름' : '주진우','소속' : '국민의힘 부산 해운대구(갑)','생년월일' : '1975.05.25','직책' : '국회의원','연락처' : '010-9004-9330','주소' : '','사진' : 'profile_photos/주진우.png',},
{'이름' : '주효정','소속' : '우리돼지국밥','생년월일' : '1971.07.29','직책' : '대표','연락처' : '010-4480-7724','주소' : '부산 동구 초량로 27-1','사진' : 'profile_photos/주효정.png',},
{'이름' : '진서윤','소속' : '㈜파나','생년월일' : '1966.04.20','직책' : '상무','연락처' : '010-3855-7758','주소' : '양산시 상북면 양산대로 1266-3 ㈜ 파나','사진' : 'profile_photos/진서윤.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '진종규','소속' : '㈜ 화승이엔씨','생년월일' : '1968.10.25','직책' : '대표','연락처' : '010-9331-4119','주소' : '부산광역시 부산진구 중앙대로 979번길 6 뉴라이웰 203호','사진' : 'profile_photos/진종규.png',},
{'이름' : '최대경','소속' : '수원대학교','생년월일' : '1961.11.12','직책' : '특임교수','연락처' : '010-3582-5164','주소' : '경기도 화성시 봉담융 와우안길 17','사진' : 'profile_photos/최대경.png',},
{'이름' : '최승자','소속' : '플로스 플라워','생년월일' : '1959.12.21','직책' : '대표','연락처' : '010-9315-4755','주소' : '부산시 중구 중앙대로 41번길 3 ','사진' : 'profile_photos/최승자.png','TITLE':'고문','SEQUENCE':'',},
{'이름' : '최유심','소속' : '㈜정수인더스트리','생년월일' : '1964.05.21','직책' : '대표','연락처' : '010-4554-6226','주소' : '부산시 부산진구 가야대로 749-1 1307호','사진' : 'profile_photos/최유심.png','TITLE':'재무국','SEQUENCE':'',},
{'이름' : '최정현','소속' : '엔피씨글로벌','생년월일' : '1979.07.12','직책' : '대표','연락처' : '010-9663-5081','주소' : '부산 강서구 공항로 1409번길 42','사진' : 'profile_photos/최정현(수정).png',},
{'이름' : '최준익','소속' : '늘바다품애','생년월일' : '1987.06.28','직책' : '대표','연락처' : '010-9798-4172','주소' : '창원시 마산합포구 어시장 4길 20','사진' : 'profile_photos/최준익.png',},
{'이름' : '최진봉','소속' : '부산시 중구','생년월일' : '1955.01.18','직책' : '구청장','연락처' : '010-4628-4002','주소' : '부산광역시 중구 중구로 120, 부산중구청 2층(구청장 비서실)','사진' : 'profile_photos/최진봉.png',},
{'이름' : '하익수','소속' : '남우건설주식회사','생년월일' : '1965.10.08','직책' : '대표','연락처' : '010-3585-0940','주소' : '부산금정구 금장로 225번길 장전벽산블루밍디자인 아파트 204동 1005호','사진' : 'profile_photos/하익수.png','TITLE':'상임부회장','SEQUENCE':'',},
{'이름' : '한윤철','소속' : '㈜ 고려엔지니어링종합건설','생년월일' : '1972.07.04','직책' : '대표','연락처' : '010-4556-0985','주소' : '부산 남구 못골로 12번길 67 4층(대연동)','사진' : 'profile_photos/한윤철.png',},
{'이름' : '허성우','소속' : '월강','생년월일' : '1979.11.13','직책' : '대표','연락처' : '010-3420-1049','주소' : '부산광역시 부산진구 서면로7 월강(1~4층)','사진' : 'profile_photos/허성우.png',},
{'이름' : '현광열','소속' : 'LG에어컨가전SW(주)','생년월일' : '1969.07.07','직책' : '대표','연락처' : '010-9926-5569','주소' : '부산시 금정구 금정로55, 5층','사진' : 'profile_photos/현광열.png',},
{'이름' : '황미영','소속' : '㈜센트럴시티','생년월일' : '1965.09.16','직책' : '대표','연락처' : '010-6780-8082','주소' : '부산시 해운대구 센텀중앙로 97, 에이동 3011호(재송동, 센텀스카이비즈)','사진' : 'profile_photos/황미영.png','TITLE':'상임부회장','SEQUENCE':'',},
{'이름' : '황순민','소속' : '부산지방국세청','생년월일' : '1969.12.12','직책' : '송무과장','연락처' : '010-9002-0026','주소' : '부산광역시 연제구 토곡로20','사진' : 'profile_photos/황순민.png',},
{'이름' : '황윤미','소속' : '㈜ 마케팅위너','생년월일' : '1976.10.09','직책' : '대표','연락처' : '010-9695-2918','주소' : '부산광역시 사상구 모라로 22, 부산벤처타워 1607호','사진' : 'profile_photos/황윤미.png',},
{'이름' : '황진순','소속' : '영진에셋 알파지점','생년월일' : '1968.04.01','직책' : '지점장','연락처' : '010-6561-5593','주소' : '부산시 부산진구 서면로22, 6층 602호(태양빌딩)','사진' : 'profile_photos/황진순.png','TITLE':'부회장','SEQUENCE':'',},
{'이름' : '황태욱','소속' : '㈜유림이엔티','생년월일' : '1982.10.27','직책' : '실장','연락처' : '010-3300-0706','주소' : '부산시 강서구 낙동북로73번길 15','사진' : 'profile_photos/황태욱.png',},
{'이름' : '황하섭','소속' : '세정강재㈜','생년월일' : '1973.12.17','직책' : '대표','연락처' : '010-5582-0078-','주소' : '부산 사상구 학장로 135번길20 (학장동)','사진' : 'profile_photos/황하섭.png',},
{'이름' : '황현숙','소속' : '정관장 가야점','생년월일' : '1961.04.27','직책' : '대표','연락처' : '010-3851-0187','주소' : '부산시 부산진구 가야대로 679번길 155 유림상가 정관장','사진' : 'profile_photos/황현숙.png','TITLE':'고문','SEQUENCE':'',},
{'이름' : '황현종','소속' : '더와이즈 법률사무소','생년월일' : '1983.10.24','직책' : '대표변호사','연락처' : '010-5466-9173','주소' : '부산광역시 연제구 법원로 28,1305호(거제동, 부산법조타운빌딩)','사진' : 'profile_photos/황현종.png','TITLE':'재무국','SEQUENCE':'',},
# {'이름' : '허남식','소속' : '신라대학교','직책' : '총장','연락처' : '010-9568-3579','주소' : 'president@silla.ac.kr','사진' : 'profile_photos/허남식.png','TITLE':'총장','SEQUENCE':'1',},
# {'이름' : '이희태','소속' : '신라대학교','직책' : '대학원장','연락처' : '010-3866-4694','주소' : '부산광역시 사상구 백양대로 700번길 140 신라대학교 대학본부 603호','사진' : 'profile_photos/이희태.png','TITLE':'대학원장','SEQUENCE':'2',},
# {'이름' : '최두원','소속' : '신라대학교','직책' : '부원장','연락처' : '010-2564-1741','주소' : '부산광역시 사상구 백양대로 700번길 140 신라대학교 공학관 910호','사진' : 'profile_photos/최두원.png','TITLE':'부원장','SEQUENCE':'3',},
]

View File

@ -0,0 +1,61 @@
#!/usr/bin/env python
"""
기존 Person 데이터의 전화번호를 대시 있는 형태로 되돌리는 스크립트
"""
import os
import sys
import django
import re
# Django 설정
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'A_core.settings')
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
django.setup()
from B_main.models import Person
def format_phone_with_dash(phone):
"""전화번호를 010-XXXX-XXXX 형식으로 변환"""
if not phone:
return phone
# 숫자만 추출
numbers = re.sub(r'[^0-9]', '', phone)
# 11자리인 경우에만 포맷팅
if len(numbers) == 11 and numbers.startswith('010'):
return f"{numbers[:3]}-{numbers[3:7]}-{numbers[7:]}"
elif len(numbers) == 10 and numbers.startswith('010'):
return f"{numbers[:3]}-{numbers[3:6]}-{numbers[6:]}"
return phone
def restore_phone_dashes():
"""기존 Person 데이터의 전화번호를 대시 있는 형태로 되돌리기"""
print("=" * 60)
print("Person 데이터 전화번호 대시 복원")
print("=" * 60)
# 모든 Person 데이터 조회
persons = Person.objects.all()
updated_count = 0
for person in persons:
if person.연락처:
old_phone = person.연락처
new_phone = format_phone_with_dash(old_phone)
if old_phone != new_phone:
print(f"복원: {person.이름} - {old_phone}{new_phone}")
person.연락처 = new_phone
person.save()
updated_count += 1
else:
print(f"변경 없음: {person.이름} - {old_phone}")
else:
print(f"전화번호 없음: {person.이름}")
print(f"\n{updated_count}개의 전화번호가 복원되었습니다.")
if __name__ == '__main__':
restore_phone_dashes()

49
B_main/show_all_users.py Normal file
View File

@ -0,0 +1,49 @@
#!/usr/bin/env python
"""
전체 유저의 메인페이지 표시를 '표시' 변경하는 스크립트
"""
import os
import sys
import django
# Django 설정
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'A_core.settings')
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
django.setup()
from B_main.models import Person
def show_all_users():
"""전체 유저의 메인페이지 표시를 '표시'로 변경"""
print("=" * 60)
print("전체 유저 메인페이지 표시 설정")
print("=" * 60)
# 모든 Person 데이터 조회
persons = Person.objects.all()
updated_count = 0
for person in persons:
if not person.보일지여부:
print(f"표시로 변경: {person.이름} (회원가입상태: {person.회원가입상태})")
person.보일지여부 = True
person.save()
updated_count += 1
else:
print(f"이미 표시: {person.이름} (회원가입상태: {person.회원가입상태})")
print(f"\n{updated_count}개의 사용자가 표시로 변경되었습니다.")
# 최종 통계
total_persons = Person.objects.count()
visible_persons = Person.objects.filter(보일지여부=True).count()
hidden_persons = Person.objects.filter(보일지여부=False).count()
print(f"\n최종 통계:")
print(f" 전체 사용자: {total_persons}")
print(f" 표시 사용자: {visible_persons}")
print(f" 숨김 사용자: {hidden_persons}")
if __name__ == '__main__':
show_all_users()

View File

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="ko" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>회원가입 | 신라 AMP</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif']
}
}
}
};
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet" />
</head>
<body class="bg-gradient-to-br from-gray-900 via-gray-800 to-black text-white min-h-screen flex items-center justify-center px-4 font-sans">
<div class="bg-gray-800 bg-opacity-70 backdrop-blur-lg p-8 rounded-2xl shadow-2xl w-full max-w-sm transition-all">
<div class="text-center mb-6">
<h1 class="text-3xl font-bold tracking-tight text-white">신라 AMP</h1>
<p class="text-sm text-gray-400 mt-2">회원가입</p>
</div>
<form method="POST" class="space-y-4">
{% csrf_token %}
{% if form.errors %}
<div class="text-red-400 text-sm mb-2">
{% for field, errors in form.errors.items %}
{% for error in errors %}
{{ error }}
{% endfor %}
{% endfor %}
</div>
{% endif %}
<div>
<label for="{{ form.email.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form.email.label }}</label>
{{ form.email }}
</div>
<div>
<label for="{{ form.password1.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form.password1.label }}</label>
{{ form.password1 }}
</div>
<div>
<label for="{{ form.password2.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form.password2.label }}</label>
{{ form.password2 }}
</div>
<div class="flex items-start space-x-3">
{{ form.privacy_agreement }}
<div class="flex-1">
<label for="{{ form.privacy_agreement.id_for_label }}" class="block text-sm text-gray-300 cursor-pointer">
{{ form.privacy_agreement.label }}
</label>
{% if form.privacy_agreement.help_text %}
<p class="text-xs text-gray-400 mt-1">{{ form.privacy_agreement.help_text }}</p>
{% endif %}
</div>
</div>
<!-- 숨겨진 필드들 -->
{{ form.name }}
{{ form.phone }}
<button type="submit"
class="w-full py-3 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 rounded-xl text-white font-semibold text-base transition duration-200 shadow-md hover:shadow-lg">
회원가입
</button>
</form>
<div class="mt-6 text-center text-sm">
<a href="{% url 'account_login' %}" class="text-blue-400 hover:text-blue-500 transition">
로그인으로 돌아가기
</a>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,39 @@
{% load static %}
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>신라대학교 AMP 제8기</title>
<script src="https://unpkg.com/htmx.org@1.9.5"></script>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-900 text-gray-100 min-h-screen">
<div class="max-w-5xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold text-center mb-6">신라대학교 AMP 제8기</h1>
<!-- 검색창 -->
<div class="mb-6">
<input
id="search-input"
type="text"
name="search"
placeholder="검색..."
class="w-full px-4 py-2 rounded-lg bg-gray-800 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
hx-get="/search/"
hx-trigger="keyup changed delay:200ms"
hx-target="#card-container"
hx-include="#search-input"
autocomplete="off"
>
</div>
<!-- 카드 목록 -->
<div id="card-container" class="grid grid-cols-1 sm:grid-cols-2 gap-4">
{% for person in people %}
{% include 'B_main/partials/card.htm' %}
{% endfor %}
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,211 @@
{% load static %}
<!DOCTYPE html>
<html lang="ko" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>신라대학교 AMP 제8기</title>
<script src="https://unpkg.com/htmx.org@1.9.5"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
}
</script>
<!-- 다크모드 초기 설정 스크립트 (FOUC 방지) -->
<script>
(function() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
document.documentElement.classList.remove('dark');
}
})();
</script>
</head>
<body class="bg-gray-200 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen transition-colors duration-300">
<div class="max-w-5xl mx-auto px-4 py-8">
<!-- 헤더와 다크모드 토글 -->
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">신라대학교 AMP 제8기</h1>
<div class="space-x-4 text-sm">
{% if user.is_authenticated %}
<div class="flex flex-col items-end sm:items-center sm:flex-row sm:space-x-4 space-y-1 sm:space-y-0">
<!-- 데스크탑에서만 환영 메시지 표시 -->
<span class="hidden sm:block text-sm text-gray-700 dark:text-gray-200 font-semibold">
{% if user.is_superuser %}
<a href="{% url 'accounts:custom_profile_edit' %}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">{{ user.username }} 님</a>, 환영합니다!
{% else %}
<a href="{% url 'accounts:custom_profile_edit' %}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">{{ user.person.이름 }} 님</a>, 환영합니다!
{% endif %}
</span>
<!-- 모바일: 햄버거 메뉴, 데스크탑: 버튼 노출 -->
<div class="relative">
<!-- 모바일: 다크모드 토글 버튼과 햄버거 버튼을 가로로 배치 -->
<div class="sm:hidden flex items-center space-x-2">
<!-- 모바일: 다크모드 토글 버튼 (햄버거 버튼 왼쪽) -->
<button id="theme-toggle-mobile" class="p-2 rounded-lg bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors" aria-label="테마 변경">
<!-- 라이트 모드 아이콘 (다크모드일 때 보임) -->
<svg id="theme-toggle-light-icon-mobile" class="w-5 h-5 text-gray-800 dark:text-gray-200" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"></path>
</svg>
<!-- 다크 모드 아이콘 (라이트모드일 때 보임) -->
<svg id="theme-toggle-dark-icon-mobile" class="w-5 h-5 text-gray-800 dark:text-gray-200 hidden" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
</button>
<!-- 햄버거 버튼 (모바일에서만 보임) -->
<button id="mobile-menu-button" class="flex items-center px-2 py-1 border rounded text-gray-700 dark:text-gray-200 border-gray-400 dark:border-gray-600 focus:outline-none" aria-label="메뉴 열기">
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
</div>
<!-- 모바일 드롭다운 메뉴 -->
<div id="mobile-menu-dropdown" class="hidden absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg z-50 border border-gray-200 dark:border-gray-700">
<!-- 모바일에서 환영 메시지 표시 -->
<div class="px-4 py-2 text-sm text-gray-700 dark:text-gray-200 font-semibold border-b border-gray-200 dark:border-gray-700">
{% if user.is_superuser %}
<a href="{% url 'accounts:custom_profile_edit' %}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">{{ user.username }} 님</a>, 환영합니다!
{% else %}
<a href="{% url 'accounts:custom_profile_edit' %}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">{{ user.person.이름 }} 님</a>, 환영합니다!
{% endif %}
</div>
<a href="{% url 'account_logout' %}" class="block px-4 py-2 text-red-500 hover:bg-gray-100 dark:hover:bg-gray-700">로그아웃</a>
</div>
<!-- 데스크탑 버튼 (sm 이상에서만 보임) -->
<div class="hidden sm:flex items-center space-x-3 mt-1 sm:mt-0">
<a href="{% url 'account_logout' %}" class="text-red-400 hover:text-red-500">로그아웃</a>
<!-- 데스크탑: 다크모드 토글 버튼 (로그아웃 오른쪽) -->
<button id="theme-toggle" class="p-2 rounded-lg bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors" aria-label="테마 변경">
<!-- 라이트 모드 아이콘 (다크모드일 때 보임) -->
<svg id="theme-toggle-light-icon" class="w-5 h-5 text-gray-800 dark:text-gray-200" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"></path>
</svg>
<!-- 다크 모드 아이콘 (라이트모드일 때 보임) -->
<svg id="theme-toggle-dark-icon" class="w-5 h-5 text-gray-800 dark:text-gray-200 hidden" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
</button>
</div>
</div>
<script>
// 햄버거 메뉴 토글 스크립트
document.addEventListener('DOMContentLoaded', function() {
const btn = document.getElementById('mobile-menu-button');
const menu = document.getElementById('mobile-menu-dropdown');
if (btn && menu) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
menu.classList.toggle('hidden');
});
// 메뉴 외부 클릭 시 닫기
document.addEventListener('click', function(e) {
if (!menu.classList.contains('hidden')) {
menu.classList.add('hidden');
}
});
// 메뉴 클릭 시 닫히지 않도록
menu.addEventListener('click', function(e) {
e.stopPropagation();
});
}
});
</script>
</div>
{% elif request.session.authenticated %}
<a href="{% url 'session_logout' %}" class="text-red-400 hover:text-red-500">로그아웃</a>
{% else %}
<a href="{% url 'account_login' %}" class="text-blue-400 hover:text-blue-500">로그인</a>
<a href="{% url 'account_signup' %}" class="text-green-400 hover:text-green-500">회원가입</a>
{% endif %}
</div>
</div>
<!-- 검색창 -->
<div class="mb-6">
<input
id="search-input"
type="text"
name="q"
placeholder="검색..."
class="w-full px-4 py-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 transition-colors duration-200"
hx-get="/search/"
hx-trigger="keyup changed delay:500ms"
hx-target="#card-container"
hx-include="#search-input"
autocomplete="off"
>
</div>
<!-- 카드 목록 -->
<div id="card-container" class="grid grid-cols-1 sm:grid-cols-2 gap-4">
{% for person in people %}
{% include 'B_main/partials/card.htm' %}
{% endfor %}
</div>
</div>
<script>
// 다크모드 토글 스크립트
const themeToggle = document.getElementById('theme-toggle');
const themeToggleMobile = document.getElementById('theme-toggle-mobile');
const lightIcon = document.getElementById('theme-toggle-light-icon');
const darkIcon = document.getElementById('theme-toggle-dark-icon');
const lightIconMobile = document.getElementById('theme-toggle-light-icon-mobile');
const darkIconMobile = document.getElementById('theme-toggle-dark-icon-mobile');
// 저장된 테마 확인
const savedTheme = localStorage.getItem('theme');
// 아이콘 초기 설정 함수
function updateIcons() {
const isDark = document.documentElement.classList.contains('dark');
if (isDark) {
// 다크모드일 때
if (lightIcon) lightIcon.classList.remove('hidden');
if (darkIcon) darkIcon.classList.add('hidden');
if (lightIconMobile) lightIconMobile.classList.remove('hidden');
if (darkIconMobile) darkIconMobile.classList.add('hidden');
} else {
// 라이트모드일 때
if (lightIcon) lightIcon.classList.add('hidden');
if (darkIcon) darkIcon.classList.remove('hidden');
if (lightIconMobile) lightIconMobile.classList.add('hidden');
if (darkIconMobile) darkIconMobile.classList.remove('hidden');
}
}
// 초기 아이콘 설정
updateIcons();
// 테마 토글 함수
function toggleTheme() {
const isDark = document.documentElement.classList.contains('dark');
if (isDark) {
// 라이트 모드로 전환
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
} else {
// 다크 모드로 전환
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
}
updateIcons();
}
// 데스크탑 토글 버튼 클릭 이벤트
if (themeToggle) {
themeToggle.addEventListener('click', toggleTheme);
}
// 모바일 토글 버튼 클릭 이벤트
if (themeToggleMobile) {
themeToggleMobile.addEventListener('click', toggleTheme);
}
</script>
</body>
</html>

View File

@ -0,0 +1,123 @@
{% load static %}
<div class="flex bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 h-[210px] relative border border-gray-200 dark:border-gray-700 transition-colors duration-300">
<!-- 좌측: 사진 + 버튼 -->
<div class="flex flex-col items-center mr-4 w-[150px]">
{% if person.사진 and person.사진.url and 'media/' in person.사진.url %}
<img
src="{{ person.사진.url }}"
alt="{{ person.이름 }}"
class="w-[150px] h-[150px] object-cover rounded-lg border border-gray-300 dark:border-gray-600 mb-2 cursor-pointer transition-colors duration-300"
onclick="openModal(this.src)"
>
{% else %}
<img
src="{% static 'B_main/images/default_user.png' %}"
alt="{{ person.이름 }}"
class="w-[150px] h-[150px] object-cover rounded-lg border border-gray-300 dark:border-gray-600 mb-2 cursor-pointer transition-colors duration-300"
onclick="openModal(this.src)"
>
{% endif %}
{% if person.이름 %}
<a
href="{% url 'vcard_download' person.이름 %}"
class="inline-block bg-blue-600 dark:bg-blue-900 text-white text-xs px-3 py-1 rounded hover:bg-blue-700 dark:hover:bg-blue-800 text-center transition-colors duration-200"
>
📇연락처저장
</a>
{% endif %}
</div>
<!-- 우측: 텍스트 정보 -->
<div class="flex flex-col justify-start w-full overflow-hidden">
<div class="flex justify-between items-baseline">
<div class="mb-4">
<h2 class="text-lg font-semibold truncate leading-tight flex-1 {% if person.user %}text-gray-900 dark:text-gray-100{% else %}text-blue-600 dark:text-blue-400{% endif %}">
{{ person.이름 }}
{% if person.생년월일 %}
<span class="text-sm text-gray-500 dark:text-gray-400 ml-1">({{ person.생년월일|date:"Y년 m월 d일" }})</span>
{% endif %}
</h2>
</div>
{% if person.TITLE %}
{% if person.TITLE == '회장' or person.TITLE == '수석부회장' or person.TITLE == '선임상임부회장' or person.TITLE == '고문회장'%}
<span class="text-xs text-blue-600 dark:text-blue-400 font-bold">
{{ person.TITLE }}
</span>
{% elif "경조국" in person.TITLE or "기획사무국" in person.TITLE or "홍보국" in person.TITLE or "친교문화국" in person.TITLE or "재무국" in person.TITLE %}
<span class="text-xs text-yellow-600 dark:text-yellow-500 font-semibold">
{{ person.TITLE }}
</span>
{% else %}
<span class="text-xs text-violet-600 dark:text-violet-400 font-semibold">
{{ person.TITLE }}
</span>
{% endif %}
{% endif %}
</div>
<div class="mt-1 text-xs leading-snug overflow-hidden max-h-[138px] flex flex-col gap-y-1">
<div class="flex mb-1">
<span class="w-[60px] text-gray-500 dark:text-gray-400">소속:</span>
<span class="flex-1 truncate text-gray-800 dark:text-gray-200">{{ person.소속 }}</span>
</div>
<div class="flex mb-1">
<span class="w-[60px] text-gray-500 dark:text-gray-400">직책:</span>
<span class="flex-1 truncate text-gray-800 dark:text-gray-200">{{ person.직책 }}</span>
</div>
<div class="flex mb-1">
<span class="w-[60px] text-gray-500 dark:text-gray-400">연락처:</span>
<span class="flex-1 truncate text-gray-800 dark:text-gray-200">{{ person.연락처 }}</span>
</div>
<div class="flex">
{% if person.이름 == '허남식' %}
<span class="w-[60px] text-gray-500 dark:text-gray-400">이메일:</span>
{% else %}
<span class="w-[60px] text-gray-500 dark:text-gray-400">주소:</span>
{% endif %}
<span class="flex-1 text-gray-800 dark:text-gray-200">{{ person.주소 }}</span>
</div>
</div>
</div>
</div>
<!-- 📸 모달 컴포넌트 -->
<div id="image-modal" class="fixed inset-0 bg-black bg-opacity-70 dark:bg-black dark:bg-opacity-80 z-50 flex items-center justify-center hidden transition-opacity duration-300" onclick="closeModal()">
<div class="relative">
<img id="modal-image" src="" class="max-w-[90vh] h-[60vh] w-auto rounded-lg border-4 border-white dark:border-gray-300 shadow-xl" alt="확대 이미지">
<!-- 닫기 버튼 -->
<button
onclick="closeModal()"
class="absolute top-2 right-2 bg-white dark:bg-gray-800 text-gray-800 dark:text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-200"
aria-label="닫기"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<script>
function openModal(src) {
const modal = document.getElementById('image-modal');
const modalImg = document.getElementById('modal-image');
modalImg.src = src;
modal.classList.remove('hidden');
// 스크롤 방지
document.body.style.overflow = 'hidden';
}
function closeModal() {
const modal = document.getElementById('image-modal');
modal.classList.add('hidden');
// 스크롤 복원
document.body.style.overflow = 'auto';
}
// ESC 키로 모달 닫기
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeModal();
}
});
</script>

View File

@ -0,0 +1,4 @@
{% load static %}
{% for person in people %}
{% include 'B_main/partials/card.htm' %}
{% endfor %}

View File

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="ko" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Enter Access Code | 신라 AMP</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif']
}
}
}
};
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet" />
</head>
<body class="bg-gradient-to-br from-gray-900 via-gray-800 to-black text-white min-h-screen flex items-center justify-center px-4 font-sans">
<div class="bg-gray-800 bg-opacity-70 backdrop-blur-lg p-8 rounded-2xl shadow-2xl w-full max-w-sm transition-all">
<div class="text-center mb-6">
<h1 class="text-3xl font-bold tracking-tight text-white">신라 AMP</h1>
<p class="text-sm text-gray-400 mt-2">Enter access code to continue</p>
</div>
<form method="POST" action="/password/">
{% csrf_token %}
<div class="mb-5">
<input
type="password"
name="password"
placeholder="Access code..."
class="w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
/>
</div>
<button
type="submit"
class="w-full py-3 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 rounded-xl text-white font-semibold text-base transition duration-200 shadow-md hover:shadow-lg"
>
Enter
</button>
</form>
<div class="mt-6 flex justify-center space-x-4 text-sm">
<a href="{% url 'account_login' %}" class="text-blue-400 hover:text-blue-500 transition">
로그인
</a>
<span class="text-gray-500">|</span>
<a href="{% url 'account_signup' %}" class="text-green-400 hover:text-green-500 transition">
회원가입
</a>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,114 @@
<!DOCTYPE html>
<html lang="ko" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>전화번호 인증 | 신라 AMP</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif']
}
}
}
};
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet" />
</head>
<body class="bg-gradient-to-br from-gray-900 via-gray-800 to-black text-white min-h-screen flex items-center justify-center px-4 font-sans">
<div class="bg-gray-800 bg-opacity-70 backdrop-blur-lg p-8 rounded-2xl shadow-2xl w-full max-w-sm transition-all">
<div class="text-center mb-6">
<h1 class="text-3xl font-bold tracking-tight text-white">신라 AMP</h1>
<p class="text-sm text-gray-400 mt-2">전화번호 인증</p>
</div>
<form method="POST" class="space-y-4">
{% csrf_token %}
{% if form.errors %}
<div class="text-red-400 text-sm mb-2">
{% for field, errors in form.errors.items %}
{% for error in errors %}
{{ error }}
{% endfor %}
{% endfor %}
</div>
{% endif %}
<div>
<label for="{{ form.name.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form.name.label }}</label>
{{ form.name }}
</div>
<div>
<label for="{{ form.phone.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form.phone.label }}</label>
{{ form.phone }}
</div>
<div>
<label for="{{ form.verification_code.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form.verification_code.label }}</label>
{{ form.verification_code }}
</div>
<button type="submit"
class="w-full py-3 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 rounded-xl text-white font-semibold text-base transition duration-200 shadow-md hover:shadow-lg">
인증 확인
</button>
</form>
<div class="mt-6 text-center text-sm">
<a href="{% url 'account_login' %}" class="text-blue-400 hover:text-blue-500 transition">
로그인으로 돌아가기
</a>
</div>
</div>
<script>
// 이름 선택 시 전화번호 자동 입력
document.getElementById('{{ form.name.id_for_label }}').addEventListener('change', function() {
const name = this.value;
const phoneField = document.getElementById('{{ form.phone.id_for_label }}');
if (name) {
// AJAX로 전화번호 가져오기
fetch(`/send-verification-code/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: JSON.stringify({
name: name,
phone: phoneField.value
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 전화번호 자동 입력 (실제로는 서버에서 가져와야 함)
const phoneMap = {
'김봉수': '010-3343-3319',
'강유천': '010-9845-4512',
'김기재': '010-1234-5678',
'박성호': '010-2345-6789',
'이영희': '010-3456-7890',
'최정현': '010-4567-8901',
'정석민': '010-5678-9012',
'황태욱': '010-6789-0123',
'임창섭': '010-7890-1234',
'서강섭': '010-8901-2345'
};
phoneField.value = phoneMap[name] || '';
}
});
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,217 @@
<!DOCTYPE html>
<html lang="ko" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>프로필 수정 | 신라 AMP</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif']
}
}
}
};
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet" />
<style>
/* "Currently:" 텍스트 숨기기 */
.help {
display: none !important;
}
/* 파일 입력 필드 스타일링 */
input[type="file"] {
border: 1px solid #4a5568;
padding: 8px;
border-radius: 4px;
background-color: #2d3748;
color: white;
width: 100%;
}
input[type="file"]::-webkit-file-upload-button {
background-color: #4299e1;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
input[type="file"]::-webkit-file-upload-button:hover {
background-color: #3182ce;
}
</style>
</head>
<body class="bg-gradient-to-br from-gray-900 via-gray-800 to-black text-white min-h-screen flex items-center justify-center px-4 font-sans">
<div class="bg-gray-800 bg-opacity-70 backdrop-blur-lg p-8 rounded-2xl shadow-2xl w-full max-w-md transition-all">
<div class="text-center mb-6">
<h1 class="text-3xl font-bold tracking-tight text-white">신라 AMP</h1>
<p class="text-sm text-gray-400 mt-2">프로필 수정</p>
</div>
<form method="POST" enctype="multipart/form-data" class="space-y-4">
{% csrf_token %}
{% if form.errors %}
<div class="text-red-400 text-sm mb-2">
{% for field, errors in form.errors.items %}
{% for error in errors %}
{{ error }}
{% endfor %}
{% endfor %}
</div>
{% endif %}
<div>
<label for="{{ form.이름.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form.이름.label }}</label>
{{ form.이름 }}
</div>
<div>
<label for="{{ form.소속.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form.소속.label }}</label>
{{ form.소속 }}
</div>
<div>
<label for="{{ form.직책.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form.직책.label }}</label>
{{ form.직책 }}
</div>
<div>
<label for="{{ form.연락처.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form.연락처.label }}</label>
{{ form.연락처 }}
</div>
<div>
<label for="{{ form.주소.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form.주소.label }}</label>
{{ form.주소 }}
</div>
<div>
<label for="{{ form.생년월일.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form.생년월일.label }}</label>
{{ form.생년월일 }}
</div>
<div>
<label for="{{ form.사진.id_for_label }}" class="block mb-1 text-sm text-gray-300">프로필 사진</label>
<!-- 커스텀 파일 입력 필드 -->
<input type="file"
name="{{ form.사진.name }}"
id="{{ form.사진.id_for_label }}"
accept="image/*"
class="w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition">
{% if form.instance.사진 and form.instance.사진.url %}
<div class="mt-2">
<img id="profile-preview" src="{{ form.instance.사진.url }}" alt="프로필 사진 미리보기" class="w-24 h-24 rounded-full object-cover border-2 border-gray-500" />
</div>
{% else %}
<div class="mt-2">
<img id="profile-preview" src="/static/B_main/images/default_user.png" alt="프로필 사진 미리보기" class="w-24 h-24 rounded-full object-cover border-2 border-gray-500" />
</div>
{% endif %}
</div>
<!-- 키워드 섹션 -->
<div class="border-t border-gray-600 pt-4">
<h3 class="text-lg font-semibold text-blue-400 mb-3">검색 키워드</h3>
<p class="text-sm text-gray-400 mb-4">다른 사람들이 당신을 찾을 수 있도록 키워드를 설정하세요</p>
<div class="mb-3">
{{ form.keyword1 }}
</div>
</div>
<button type="submit"
class="w-full py-3 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 rounded-xl text-white font-semibold text-base transition duration-200 shadow-md hover:shadow-lg">
프로필 저장
</button>
</form>
<!-- 회원탈퇴 섹션 -->
<div class="mt-8 pt-6 border-t border-gray-600">
<div class="text-center">
<h3 class="text-lg font-semibold text-red-400 mb-2">회원탈퇴</h3>
<p class="text-sm text-gray-400 mb-4">
탈퇴하시면 로그인이 불가능하며, 개인정보는 보존됩니다.
</p>
<button
type="button"
onclick="confirmWithdrawal()"
class="px-6 py-2 bg-red-600 hover:bg-red-700 active:bg-red-800 rounded-lg text-white font-medium text-sm transition duration-200 shadow-md hover:shadow-lg">
회원탈퇴
</button>
</div>
</div>
<!-- 비밀번호 변경 섹션 -->
<div class="mt-6 pt-6 border-t border-gray-600">
<div class="text-center">
<h3 class="text-lg font-semibold text-yellow-400 mb-2">비밀번호 변경</h3>
<p class="text-sm text-gray-400 mb-4">
전화번호 인증을 통해 비밀번호를 변경할 수 있습니다.
</p>
<a href="{% url 'accounts:password_change' %}"
class="inline-block px-6 py-2 bg-yellow-600 hover:bg-yellow-700 rounded-lg text-white font-medium text-sm transition duration-200 shadow-md hover:shadow-lg">
비밀번호 변경
</a>
</div>
</div>
<div class="mt-6 text-center text-sm">
<a href="{% url 'main' %}" class="text-blue-400 hover:text-blue-500 transition">
메인으로 돌아가기
</a>
</div>
</div>
<script>
// 사진 업로드 시 미리보기
document.querySelector('input[type=file][name$=사진]').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(ev) {
document.getElementById('profile-preview').src = ev.target.result;
};
reader.readAsDataURL(file);
}
});
// 회원탈퇴 확인
function confirmWithdrawal() {
if (confirm('정말로 회원탈퇴를 하시겠습니까?\n\n탈퇴하시면 로그인이 불가능하며, 개인정보는 보존됩니다.')) {
// CSRF 토큰 가져오기
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
// 탈퇴 요청 전송
fetch('{% url "withdraw" %}', {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
})
.then(response => {
console.log('Response status:', response.status);
return response.json();
})
.then(data => {
console.log('Response data:', data);
if (data.success) {
alert('회원탈퇴가 완료되었습니다.');
window.location.href = '{% url "main" %}';
} else {
alert('회원탈퇴 중 오류가 발생했습니다: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('회원탈퇴 중 오류가 발생했습니다.');
});
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,135 @@
<!DOCTYPE html>
<html lang="ko" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>회원가입 | 신라 AMP</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif']
}
}
}
};
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet" />
</head>
<body class="bg-gradient-to-br from-gray-900 via-gray-800 to-black text-white min-h-screen flex items-center justify-center px-4 font-sans">
<div class="bg-gray-800 bg-opacity-70 backdrop-blur-lg p-8 rounded-2xl shadow-2xl w-full max-w-sm transition-all">
<div class="text-center mb-6">
<h1 class="text-3xl font-bold tracking-tight text-white">신라 AMP</h1>
<p class="text-sm text-gray-400 mt-2">회원 정보를 입력해 주세요</p>
</div>
{% if message %}
<div class="text-green-400 text-sm mb-4 text-center">{{ message }}</div>
{% endif %}
{% if step == 1 %}
<form method="POST" class="space-y-4" id="signup-form-step1">
{% csrf_token %}
<div>
<label for="id_name" class="block mb-1 text-sm text-gray-300">이름</label>
{{ form1.name }}
</div>
<div>
<label for="id_phone" class="block mb-1 text-sm text-gray-300">전화번호 (대시 없이 입력)</label>
<div class="flex gap-2">
{{ form1.phone }}
<button type="submit" name="action" value="send_code" id="send-code-btn"
class="px-3 py-2 bg-green-600 hover:bg-green-700 rounded-lg text-white font-semibold text-xs transition duration-200 shadow-md hover:shadow-lg"
{% if code_sent %}disabled style="opacity:0.5;cursor:not-allowed;"{% endif %}>
인증번호 발송
</button>
</div>
{% if error %}
<div class="text-red-400 text-sm mt-2">{{ error }}</div>
{% endif %}
</div>
<div>
<label for="id_verification_code" class="block mb-1 text-sm text-gray-300">인증번호</label>
<div class="flex gap-2">
{{ form1.verification_code }}
<button type="submit" name="action" value="verify_code" id="verify-code-btn"
class="px-3 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-white font-semibold text-xs transition duration-200 shadow-md hover:shadow-lg"
{% if not code_sent %}disabled style="opacity:0.5;cursor:not-allowed;"{% endif %}>
인증번호 확인
</button>
</div>
</div>
</form>
{% elif step == 2 %}
<form method="POST" class="space-y-4" id="signup-form-step2">
{% csrf_token %}
{% if form2.errors %}
<div class="text-red-400 text-sm mb-2">
{% for field, errors in form2.errors.items %}
{% for error in errors %}
{{ error }}
{% endfor %}
{% endfor %}
</div>
{% endif %}
<div>
<label class="block mb-1 text-sm text-gray-300">이름</label>
<input type="text" value="{{ name }}" readonly class="w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-gray-400 border border-gray-600" />
</div>
<div>
<label class="block mb-1 text-sm text-gray-300">전화번호</label>
<input type="text" value="{{ phone }}" readonly class="w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-gray-400 border border-gray-600" />
</div>
<div>
<label for="id_password1" class="block mb-1 text-sm text-gray-300">Password</label>
{{ form2.password1 }}
</div>
<div>
<label for="id_password2" class="block mb-1 text-sm text-gray-300">Password (again)</label>
{{ form2.password2 }}
</div>
<div class="bg-gray-700 bg-opacity-50 p-4 rounded-xl border border-gray-600">
<div class="flex items-start space-x-4">
{{ form2.privacy_agreement }}
<div class="flex-1">
<label for="{{ form2.privacy_agreement.id_for_label }}" class="block text-base font-semibold text-white cursor-pointer mb-2">
{{ form2.privacy_agreement.label }}
</label>
</div>
</div>
{% if form2.privacy_agreement.help_text %}
<div class="text-sm text-gray-300 leading-relaxed text-left mt-3 ml-3">
<p class="mb-2 text-left">다음 정보의 공개에 동의합니다:</p>
<ul class="list-disc list-inside space-y-1 text-gray-300 text-left">
<li>이름</li>
<li>생년월일</li>
<li>소속</li>
<li>직책</li>
<li>연락처</li>
<li>주소</li>
<li>사진</li>
</ul>
<p class="mt-3 text-yellow-300 font-medium text-left">※ 위 정보는 신라 AMP 제8기 수강생들 간에 공유됩니다.</p>
</div>
{% endif %}
</div>
<button type="submit" class="w-full py-3 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 rounded-xl text-white font-semibold text-base transition duration-200 shadow-md hover:shadow-lg">
회원가입
</button>
</form>
{% endif %}
<div class="mt-6 text-center text-sm">
이미 계정이 있으신가요? <a href="{% url 'account_login' %}" class="text-blue-400 hover:text-blue-500 transition">로그인</a>
</div>
</div>
</body>
</html>

3
B_main/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,79 @@
#!/usr/bin/env python
"""
기존 Person 데이터의 전화번호를 대시 없는 형태로 업데이트하는 스크립트
"""
import os
import sys
import django
import re
# Django 설정
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'A_core.settings')
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
django.setup()
from B_main.models import Person
def format_phone_number(phone):
"""전화번호에서 숫자만 추출하여 반환"""
if not phone:
return phone
# 숫자만 추출
numbers = re.sub(r'[^0-9]', '', phone)
# 11자리인 경우에만 반환
if len(numbers) == 11 and numbers.startswith('010'):
return numbers
elif len(numbers) == 10 and numbers.startswith('010'):
return numbers
return phone
def update_phone_numbers():
"""기존 Person 데이터의 전화번호를 대시 없는 형태로 업데이트"""
print("=" * 60)
print("Person 데이터 전화번호 업데이트")
print("=" * 60)
# 모든 Person 데이터 조회
persons = Person.objects.all()
updated_count = 0
for person in persons:
if person.연락처:
old_phone = person.연락처
new_phone = format_phone_number(old_phone)
if old_phone != new_phone:
print(f"업데이트: {person.이름} - {old_phone}{new_phone}")
person.연락처 = new_phone
person.save()
updated_count += 1
else:
print(f"변경 없음: {person.이름} - {old_phone}")
else:
print(f"전화번호 없음: {person.이름}")
print(f"\n{updated_count}개의 전화번호가 업데이트되었습니다.")
# 중복 확인
print("\n중복 전화번호 확인:")
phone_counts = {}
for person in Person.objects.all():
if person.연락처:
phone_counts[person.연락처] = phone_counts.get(person.연락처, 0) + 1
duplicates = {phone: count for phone, count in phone_counts.items() if count > 1}
if duplicates:
print("중복된 전화번호 발견:")
for phone, count in duplicates.items():
print(f" {phone}: {count}")
persons_with_phone = Person.objects.filter(연락처=phone)
for person in persons_with_phone:
print(f" - {person.이름} (ID: {person.id}, 회원가입상태: {person.회원가입상태})")
else:
print("중복된 전화번호가 없습니다.")
if __name__ == '__main__':
update_phone_numbers()

19
B_main/urls.py Normal file
View File

@ -0,0 +1,19 @@
from django.urls import path
from . import views
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('', views.main, name='main'),
path('vcard/<str:name>/', views.vcard_download, name='vcard_download'),
path('search/', views.search_people, name='search_people'),
path('password/', views.password_required, name='password_required'),
path('logout/', views.logout_view, name='logout'),
path('my-profile/', views.my_profile, name='my_profile'),
path('withdraw/', views.withdraw, name='withdraw'),
path('session_logout/', views.session_logout, name='session_logout'),
path('signup/', views.signup_view, name='signup'),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

373
B_main/views.py Normal file
View File

@ -0,0 +1,373 @@
from django.shortcuts import render, redirect
from django.http import HttpResponse, JsonResponse
from urllib.parse import unquote
from .models import Person
from django.db import models
from django.contrib.auth.decorators import login_required
from .forms import PersonForm, Step1PhoneForm, Step2AccountForm
from .models import Person
from django.shortcuts import get_object_or_404
from django.db.models import Q, Case, When, Value, IntegerField
from django.contrib.auth import login, logout
import random
import json
def password_required(request):
PASSWORD = '1110' # 실제 비밀번호
# 디버깅을 위한 로그
print(f"[DEBUG] password_required - user.is_authenticated: {request.user.is_authenticated}")
print(f"[DEBUG] password_required - user: {request.user}")
# 로그인이 된 사용자는 바로 메인 페이지로 리다이렉트
if request.user.is_authenticated:
next_url = request.GET.get("next", "/")
if not next_url:
next_url = "/"
print(f"[DEBUG] User is authenticated, redirecting to: {next_url}")
return redirect(next_url)
if request.method == "POST":
entered_password = request.POST.get("password")
if entered_password == PASSWORD:
request.session["authenticated"] = True
next_url = request.POST.get("next", "/")
if not next_url:
next_url = "/"
return redirect(next_url)
else:
return render(request, "B_main/password.htm", {"error": "Incorrect password. Please try again."})
# GET 요청 시 비밀번호 입력 폼 렌더링
next_url = request.GET.get("next", "/")
return render(request, "B_main/password.htm", {"next": next_url})
# 인증 검사 함수
def check_authentication(request):
# 디버깅을 위한 로그
print(f"[DEBUG] check_authentication - user.is_authenticated: {request.user.is_authenticated}")
print(f"[DEBUG] check_authentication - session.authenticated: {request.session.get('authenticated')}")
print(f"[DEBUG] check_authentication - user: {request.user}")
# 로그인이 된 사용자는 인증 통과
if request.user.is_authenticated:
print(f"[DEBUG] User is authenticated, allowing access")
return None
# 세션 인증이 된 사용자도 통과
if request.session.get("authenticated"):
print(f"[DEBUG] Session is authenticated, allowing access")
return None
# 둘 다 안 된 경우에만 비밀번호 페이지로 리다이렉트
print(f"[DEBUG] No authentication found, redirecting to password page")
return redirect(f"/accounts/login/?next={request.path}")
def main(request):
auth_check = check_authentication(request)
if auth_check:
return auth_check
# 현재 사용자의 Person 정보 가져오기
current_user_person = None
if request.user.is_authenticated:
try:
current_user_person = Person.objects.get(user=request.user)
except Person.DoesNotExist:
pass
# 기본 필터: 이름이 있는 사람들
base_filter = Person.objects.filter(
이름__isnull=False
).exclude(
이름__exact=''
)
# 현재 사용자의 권한에 따라 추가 필터 적용
if current_user_person and not current_user_person.모든사람보기권한:
# 모든사람보기권한이 False인 경우 회원가입한 사람만 표시 (user가 있는 사람들)
base_filter = base_filter.filter(user__isnull=False)
print(f"[DEBUG] 회원가입자만 표시 모드: {current_user_person.이름}")
else:
print(f"[DEBUG] 모든 사람 표시 모드")
# 순서가 있는 항목을 먼저 보여주고, 나머지는 가나다순으로 정렬
people = base_filter.annotate(
sequence_order=Case(
When(SEQUENCE__isnull=True, then=Value(1)),
default=Value(0),
output_field=IntegerField(),
)
).order_by('sequence_order', 'SEQUENCE', '이름')
print(f"[DEBUG] 메인 페이지 표시: {people.count()}")
for person in people:
status = "회원가입" if person.user else "미가입"
print(f"[DEBUG] - {person.이름} (상태: {status})")
return render(request, 'B_main/main.htm', {'people': people})
def search_people(request):
auth_check = check_authentication(request)
if auth_check:
return auth_check
query = request.GET.get('q', '')
print(f"[DEBUG] 검색 쿼리: '{query}'")
# 현재 사용자의 Person 정보 가져오기
current_user_person = None
if request.user.is_authenticated:
try:
current_user_person = Person.objects.get(user=request.user)
except Person.DoesNotExist:
pass
# 기본 필터: 모든 사람
base_filter = Person.objects.all()
# 현재 사용자의 권한에 따라 추가 필터 적용
if current_user_person and not current_user_person.모든사람보기권한:
# 모든사람보기권한이 False인 경우 회원가입한 사람만 표시 (user가 있는 사람들)
base_filter = base_filter.filter(user__isnull=False)
print(f"[DEBUG] 검색 - 회원가입자만 표시 모드: {current_user_person.이름}")
else:
print(f"[DEBUG] 검색 - 모든 사람 표시 모드")
if query:
# 이름, 소속, 직책, 키워드로 검색
# 순서가 있는 항목을 먼저 보여주고, 나머지는 가나다순으로 정렬
people = base_filter.filter(
Q(이름__icontains=query) |
Q(소속__icontains=query) |
Q(TITLE__icontains=query) |
Q(직책__icontains=query) |
Q(keyword1__icontains=query) |
Q(생년월일__icontains=query)
).filter(
이름__isnull=False
).exclude(
이름__exact=''
).annotate(
sequence_order=Case(
When(SEQUENCE__isnull=True, then=Value(1)),
default=Value(0),
output_field=IntegerField(),
)
).order_by('sequence_order', 'SEQUENCE', '이름')
print(f"[DEBUG] 검색 결과: {people.count()}")
for person in people:
print(f"[DEBUG] - {person.이름} (소속: {person.소속}, 직책: {person.직책})")
else:
# 순서가 있는 항목을 먼저 보여주고, 나머지는 가나다순으로 정렬
people = base_filter.filter(
이름__isnull=False
).exclude(
이름__exact=''
).annotate(
sequence_order=Case(
When(SEQUENCE__isnull=True, then=Value(1)),
default=Value(0),
output_field=IntegerField(),
)
).order_by('sequence_order', 'SEQUENCE', '이름')
print(f"[DEBUG] 전체 목록: {people.count()}")
return render(request, 'B_main/partials/card_list.htm', {'people': people})
def vcard_download(request, name):
auth_check = check_authentication(request)
if auth_check:
return auth_check
name = unquote(name)
if not name:
return HttpResponse("Invalid name", status=400)
person = get_object_or_404(Person, 이름=name)
vcard_content = f"""BEGIN:VCARD
VERSION:3.0
N:{person.이름};;;;
FN:{person.이름}
ORG:{person.소속}
TITLE:{person.직책}
TEL;CELL:{person.연락처}
ADR:;;{person.주소}
END:VCARD
"""
response = HttpResponse(vcard_content, content_type='text/vcard')
response['Content-Disposition'] = f'attachment; filename="{person.이름}.vcf"'
return response
def logout_view(request):
request.session.flush()
return redirect('/password/')
@login_required
def my_profile(request):
try:
person = Person.objects.get(user=request.user)
except Person.DoesNotExist:
person = None
if request.method == 'POST':
form = PersonForm(request.POST, instance=person)
if form.is_valid():
person = form.save(commit=False)
person.user = request.user
person.save()
return redirect('main') # or any success page
else:
form = PersonForm(instance=person)
return render(request, 'B_main/profile_form.htm', {'form': form})
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
@login_required
@csrf_exempt
@require_http_methods(["POST"])
def withdraw(request):
"""회원탈퇴 뷰"""
try:
# 현재 사용자의 Person 정보 가져오기
person = Person.objects.get(user=request.user)
# User 연결 해제
person.user = None
person.save()
# User 객체 삭제 (전화번호 계정 삭제)
user_phone = request.user.username
request.user.delete()
# 로그아웃
logout(request)
print(f"[DEBUG] 회원탈퇴 완료: {user_phone} (User 삭제, Person 연결 해제)")
return JsonResponse({'success': True})
except Person.DoesNotExist:
return JsonResponse({'success': False, 'error': 'Person 정보를 찾을 수 없습니다.'})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)})
def session_logout(request):
try:
del request.session['authenticated']
except KeyError:
pass
return redirect('/')
def signup_view(request):
import random
from .forms import is_allowed_person
from django.contrib.auth import login
# GET 요청 시 세션 초기화 (새로운 회원가입 시작)
# 단, 인증번호 확인 후 리다이렉트된 경우는 세션 유지
if request.method == 'GET' and not request.session.get('signup_verified'):
for key in ['signup_code', 'signup_name', 'signup_phone', 'signup_verified', 'signup_step']:
request.session.pop(key, None)
request.session['signup_step'] = 1
request.session['signup_verified'] = False
step = request.session.get('signup_step', 1)
name = request.session.get('signup_name')
phone = request.session.get('signup_phone')
code_sent = bool(request.session.get('signup_code'))
verified = request.session.get('signup_verified', False)
# 1단계: 이름, 전화번호, 인증번호
if step == 1:
if request.method == 'POST':
form = Step1PhoneForm(request.POST)
action = request.POST.get('action')
if action == 'send_code':
if form.is_valid():
name = form.cleaned_data['name']
phone = form.cleaned_data['phone']
# 폼 검증에서 이미 허가되지 않은 사용자 체크를 했으므로 여기서는 제거
code = str(random.randint(100000, 999999))
request.session['signup_code'] = code
request.session['signup_name'] = name
request.session['signup_phone'] = phone
request.session['signup_verified'] = False
print(f"[DEBUG] 인증번호 발송: {name} ({phone}) - {code}")
return render(request, 'B_main/signup.html', {
'step': 1, 'form1': form, 'code_sent': True, 'message': '인증번호가 발송되었습니다.'
})
else:
# 폼 에러 메시지 확인
error_message = '입력 정보를 확인해주세요.'
if form.errors:
# 첫 번째 에러 메시지 사용
for field_errors in form.errors.values():
if field_errors:
error_message = field_errors[0]
break
return render(request, 'B_main/signup.html', {
'step': 1, 'form1': form, 'code_sent': False,
'error': error_message
})
elif action == 'verify_code':
if form.is_valid():
verification_code = form.cleaned_data['verification_code']
session_code = request.session.get('signup_code')
if verification_code and verification_code == session_code:
# 인증 성공
request.session['signup_verified'] = True
request.session['signup_step'] = 2
return redirect('signup')
else:
return render(request, 'B_main/signup.html', {
'step': 1, 'form1': form, 'code_sent': True, 'error': '인증번호가 올바르지 않습니다.'
})
else:
return render(request, 'B_main/signup.html', {'step': 1, 'form1': form, 'code_sent': code_sent})
else:
form = Step1PhoneForm()
return render(request, 'B_main/signup.html', {'step': 1, 'form1': form, 'code_sent': False})
# 2단계: 이메일, 비밀번호, 비밀번호 확인
if step == 2 and verified and name and phone:
if request.method == 'POST':
form2 = Step2AccountForm(request.POST)
if form2.is_valid():
user = form2.save(name, phone, request)
login(request, user, backend='django.contrib.auth.backends.ModelBackend')
# 세션 정리
for key in ['signup_code', 'signup_name', 'signup_phone', 'signup_verified', 'signup_step']:
request.session.pop(key, None)
return redirect('main')
else:
return render(request, 'B_main/signup.html', {'step': 2, 'form2': form2, 'name': name, 'phone': phone})
else:
form2 = Step2AccountForm()
return render(request, 'B_main/signup.html', {'step': 2, 'form2': form2, 'name': name, 'phone': phone})
# 기본: 1단계로 초기화
request.session['signup_step'] = 1
request.session['signup_verified'] = False
return redirect('signup')

0
C_accounts/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
C_accounts/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

10
C_accounts/apps.py Normal file
View File

@ -0,0 +1,10 @@
from django.apps import AppConfig
class CAccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'C_accounts'
def ready(self):
import C_accounts.signals

269
C_accounts/forms.py Normal file
View File

@ -0,0 +1,269 @@
from django import forms
from django.contrib.auth import get_user_model
from B_main.models import Person # 또는 Person 모델이 정의된 경로로 import
import random
import re
User = get_user_model()
def format_phone_number(phone):
"""전화번호에서 대시 제거"""
return re.sub(r'[^0-9]', '', phone)
class CustomFileInput(forms.FileInput):
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
# "Currently:" 텍스트 제거
if 'help_text' in context:
context['help_text'] = ''
return context
class ProfileFullEditForm(forms.ModelForm):
# 통합된 이름 필드 (편집 불가능)
full_name = forms.CharField(
label="이름",
required=False,
widget=forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-600 bg-opacity-80 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'readonly': 'readonly',
'placeholder': '이름'
})
)
class Meta:
model = Person
fields = [
'소속', '직책', '주소', '사진', 'keyword1'
]
widgets = {
'소속': forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '소속'
}),
'직책': forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '직책'
}),
'주소': forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '주소'
}),
'사진': CustomFileInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'accept': 'image/*'
}),
'keyword1': forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '검색 키워드 (예: 회계감사)'
}),
}
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
# 통합된 이름 설정 (first_name + last_name)
full_name = f"{self.user.first_name or ''} {self.user.last_name or ''}".strip()
self.fields['full_name'].initial = full_name
# Person 모델 필드 초기값 설정 (기존 인스턴스가 있는 경우)
if self.instance and self.instance.pk:
# 기존 Person 인스턴스의 데이터로 초기화
for field_name in self.fields:
if field_name == 'full_name':
continue
if hasattr(self.instance, field_name):
self.fields[field_name].initial = getattr(self.instance, field_name)
def save(self, commit=True):
# Person 모델 저장 (User 모델은 수정하지 않음)
if commit:
instance = super().save(commit=False)
instance.user = self.user
instance.save()
return self.user
# 모드1: 비밀번호 찾기 폼
class PasswordResetStep1Form(forms.Form):
"""비밀번호 찾기 1단계: 전화번호 인증"""
phone = forms.CharField(
max_length=11,
label='전화번호',
widget=forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '01012345678'
})
)
verification_code = forms.CharField(
max_length=6,
label='인증번호',
required=False,
widget=forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '6자리 인증번호'
})
)
def clean_phone(self):
phone = self.cleaned_data.get('phone')
if phone:
# 전화번호 포맷팅 적용 (대시 제거)
formatted_phone = format_phone_number(phone)
# 해당 전화번호로 가입된 사용자가 있는지 확인
try:
user = User.objects.get(username=formatted_phone)
return formatted_phone
except User.DoesNotExist:
raise forms.ValidationError('등록되지 않은 전화번호입니다.')
return phone
# 모드2: 로그인 상태 비밀번호 변경 폼
class PasswordChangeLoggedInForm(forms.Form):
"""로그인 상태에서 비밀번호 변경"""
current_password = forms.CharField(
label='현재 비밀번호',
widget=forms.PasswordInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '현재 비밀번호'
})
)
new_password1 = forms.CharField(
label='새 비밀번호',
widget=forms.PasswordInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '새 비밀번호'
})
)
new_password2 = forms.CharField(
label='새 비밀번호 확인',
widget=forms.PasswordInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '새 비밀번호 확인'
})
)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
def clean_current_password(self):
current_password = self.cleaned_data.get('current_password')
if self.user and not self.user.check_password(current_password):
raise forms.ValidationError('현재 비밀번호가 올바르지 않습니다.')
return current_password
def clean(self):
cleaned_data = super().clean()
password1 = cleaned_data.get('new_password1')
password2 = cleaned_data.get('new_password2')
if password1 and password2 and password1 != password2:
raise forms.ValidationError('새 비밀번호가 일치하지 않습니다.')
if password1 and len(password1) < 8:
raise forms.ValidationError('비밀번호는 최소 8자 이상이어야 합니다.')
return cleaned_data
# 모드3: 강제 비밀번호 설정 폼
class ForcePasswordSetForm(forms.Form):
"""강제 비밀번호 설정"""
new_password1 = forms.CharField(
label='새 비밀번호',
widget=forms.PasswordInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '새 비밀번호'
})
)
new_password2 = forms.CharField(
label='새 비밀번호 확인',
widget=forms.PasswordInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '새 비밀번호 확인'
})
)
def clean(self):
cleaned_data = super().clean()
password1 = cleaned_data.get('new_password1')
password2 = cleaned_data.get('new_password2')
if password1 and password2 and password1 != password2:
raise forms.ValidationError('비밀번호가 일치하지 않습니다.')
if password1 and len(password1) < 8:
raise forms.ValidationError('비밀번호는 최소 8자 이상이어야 합니다.')
return cleaned_data
# 기존 폼들 (유지)
class PasswordChangeStep1Form(forms.Form):
"""비밀번호 변경 1단계: 전화번호 인증"""
phone = forms.CharField(
max_length=11,
label='전화번호',
widget=forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '01012345678'
})
)
verification_code = forms.CharField(
max_length=6,
label='인증번호',
required=False,
widget=forms.TextInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '6자리 인증번호'
})
)
def clean(self):
cleaned_data = super().clean()
phone = cleaned_data.get('phone')
if phone:
# 전화번호 포맷팅 적용 (대시 제거)
formatted_phone = format_phone_number(phone)
cleaned_data['phone'] = formatted_phone
# 현재 로그인한 사용자의 전화번호와 일치하는지 확인
if not self.user or self.user.username != formatted_phone:
raise forms.ValidationError('등록된 전화번호와 일치하지 않습니다.')
return cleaned_data
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
class PasswordChangeStep2Form(forms.Form):
"""비밀번호 변경 2단계: 새 비밀번호 입력"""
new_password1 = forms.CharField(
label='새 비밀번호',
widget=forms.PasswordInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '새 비밀번호'
})
)
new_password2 = forms.CharField(
label='새 비밀번호 확인',
widget=forms.PasswordInput(attrs={
'class': 'w-full px-4 py-3 rounded-xl bg-gray-700 bg-opacity-80 text-white placeholder-gray-400 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition',
'placeholder': '새 비밀번호 확인'
})
)
def clean(self):
cleaned_data = super().clean()
password1 = cleaned_data.get('new_password1')
password2 = cleaned_data.get('new_password2')
if password1 and password2 and password1 != password2:
raise forms.ValidationError('비밀번호가 일치하지 않습니다.')
if password1 and len(password1) < 8:
raise forms.ValidationError('비밀번호는 최소 8자 이상이어야 합니다.')
return cleaned_data

25
C_accounts/middleware.py Normal file
View File

@ -0,0 +1,25 @@
from django.shortcuts import redirect
from django.urls import reverse
from B_main.models import Person
class ForcePasswordSetMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 로그인한 사용자이고 비밀번호 설정이 필요한 경우
if request.user.is_authenticated:
try:
person = Person.objects.get(user=request.user)
if person.비밀번호설정필요:
# 현재 URL이 강제 비밀번호 설정 페이지가 아닌 경우에만 리다이렉트
current_path = request.path
force_password_set_path = reverse('accounts:force_password_set')
if current_path != force_password_set_path and not current_path.startswith('/admin/'):
return redirect('accounts:force_password_set')
except Person.DoesNotExist:
pass
response = self.get_response(request)
return response

View File

3
C_accounts/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

24
C_accounts/signals.py Normal file
View File

@ -0,0 +1,24 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from allauth.account.signals import user_signed_up
from B_main.models import Person
@receiver(user_signed_up)
def create_person_profile(sender, request, user, **kwargs):
"""회원가입 시 Person 프로필 생성"""
try:
# 이미 Person 프로필이 있는지 확인
Person.objects.get_or_create(
user=user,
defaults={
'이름': user.get_full_name() or user.username,
'소속': '',
'직책': '',
'연락처': '',
'주소': '',
'사진': 'B_main/images/강경옥.png'
}
)
except Exception as e:
print(f"Person 프로필 생성 중 오류: {e}")

View File

@ -0,0 +1,182 @@
{% load static %}
<!DOCTYPE html>
<html lang="ko" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>비밀번호 설정 | 신라대학교 AMP 제8기</title>
<script src="https://unpkg.com/htmx.org@1.9.5"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
}
</script>
<!-- 다크모드 초기 설정 스크립트 (FOUC 방지) -->
<script>
(function() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
document.documentElement.classList.remove('dark');
}
})();
</script>
</head>
<body class="bg-gray-200 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen transition-colors duration-300">
<style>
/* "Currently:" 텍스트 숨기기 */
.help {
display: none !important;
}
/* 파일 입력 필드 스타일링 */
input[type="file"] {
border: 1px solid #4a5568;
padding: 8px;
border-radius: 4px;
background-color: #2d3748;
color: white;
width: 100%;
}
input[type="file"]::-webkit-file-upload-button {
background-color: #4299e1;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
input[type="file"]::-webkit-file-upload-button:hover {
background-color: #3182ce;
}
</style>
<div class="bg-gray-200 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen transition-colors duration-300">
<!-- 헤더 -->
<div class="max-w-5xl mx-auto px-4 py-4">
<div class="flex justify-between items-center">
<h1 class="text-3xl font-bold">신라대학교 AMP 제8기</h1>
<div class="space-x-4 text-sm">
{% if user.is_authenticated %}
<div class="flex flex-col items-end sm:items-center sm:flex-row sm:space-x-4 space-y-1 sm:space-y-0">
<!-- 데스크탑에서만 환영 메시지 표시 -->
<span class="hidden sm:block text-sm text-gray-700 dark:text-gray-200 font-semibold">
{% if user.is_superuser %}
<a href="{% url 'accounts:custom_profile_edit' %}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">{{ user.username }} 님</a>, 환영합니다!
{% else %}
<a href="{% url 'accounts:custom_profile_edit' %}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">{{ user.person.이름 }} 님</a>, 환영합니다!
{% endif %}
</span>
<!-- 모바일: 햄버거 메뉴, 데스크탑: 버튼 노출 -->
<div class="relative">
<!-- 햄버거 버튼 (모바일에서만 보임) -->
<button id="mobile-menu-button" class="sm:hidden flex items-center px-2 py-1 border rounded text-gray-700 dark:text-gray-200 border-gray-400 dark:border-gray-600 focus:outline-none" aria-label="메뉴 열기">
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
<!-- 모바일 드롭다운 메뉴 -->
<div id="mobile-menu-dropdown" class="hidden absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg z-50 border border-gray-200 dark:border-gray-700">
<!-- 모바일에서 환영 메시지 표시 -->
<div class="px-4 py-2 text-sm text-gray-700 dark:text-gray-200 font-semibold border-b border-gray-200 dark:border-gray-700">
{% if user.is_superuser %}
<a href="{% url 'accounts:custom_profile_edit' %}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">{{ user.username }} 님</a>, 환영합니다!
{% else %}
<a href="{% url 'accounts:custom_profile_edit' %}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">{{ user.person.이름 }} 님</a>, 환영합니다!
{% endif %}
</div>
<a href="{% url 'account_logout' %}" class="block px-4 py-2 text-red-500 hover:bg-gray-100 dark:hover:bg-gray-700">로그아웃</a>
</div>
<!-- 데스크탑 버튼 (sm 이상에서만 보임) -->
<div class="hidden sm:flex space-x-3 mt-1 sm:mt-0">
<a href="{% url 'account_logout' %}" class="text-red-400 hover:text-red-500">로그아웃</a>
</div>
</div>
<script>
// 햄버거 메뉴 토글 스크립트
document.addEventListener('DOMContentLoaded', function() {
const btn = document.getElementById('mobile-menu-button');
const menu = document.getElementById('mobile-menu-dropdown');
if (btn && menu) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
menu.classList.toggle('hidden');
});
// 메뉴 외부 클릭 시 닫기
document.addEventListener('click', function(e) {
if (!menu.classList.contains('hidden')) {
menu.classList.add('hidden');
}
});
// 메뉴 클릭 시 닫히지 않도록
menu.addEventListener('click', function(e) {
e.stopPropagation();
});
}
});
</script>
</div>
{% elif request.session.authenticated %}
<a href="{% url 'session_logout' %}" class="text-red-400 hover:text-red-500">로그아웃</a>
{% else %}
<a href="{% url 'account_login' %}" class="text-blue-400 hover:text-blue-500">로그인</a>
<a href="{% url 'account_signup' %}" class="text-green-400 hover:text-green-500">회원가입</a>
{% endif %}
</div>
</div>
</div>
<!-- 강제 비밀번호 설정 폼 - 화면 중앙 배치 -->
<div class="flex items-center justify-center min-h-[calc(100vh-120px)]">
<div class="bg-gray-800 bg-opacity-70 backdrop-blur-lg p-8 rounded-2xl shadow-2xl w-full max-w-md transition-all">
<div class="text-center mb-6">
<h2 class="text-2xl font-bold tracking-tight text-white">비밀번호 설정</h2>
<p class="text-sm text-gray-400 mt-2">보안을 위해 비밀번호를 설정해주세요</p>
</div>
<form method="POST" class="space-y-4">
{% csrf_token %}
{% if form.errors %}
<div class="text-red-400 text-sm mb-2">
{% for field, errors in form.errors.items %}
{% for error in errors %}
{{ error }}
{% endfor %}
{% endfor %}
</div>
{% endif %}
<div>
<label for="{{ form.new_password1.id_for_label }}" class="block text-sm font-medium text-gray-300 mb-1">
{{ form.new_password1.label }}
</label>
{{ form.new_password1 }}
{% if form.new_password1.errors %}
<p class="text-sm text-red-400 mt-1">{{ form.new_password1.errors.0 }}</p>
{% endif %}
</div>
<div>
<label for="{{ form.new_password2.id_for_label }}" class="block text-sm font-medium text-gray-300 mb-1">
{{ form.new_password2.label }}
</label>
{{ form.new_password2 }}
{% if form.new_password2.errors %}
<p class="text-sm text-red-400 mt-1">{{ form.new_password2.errors.0 }}</p>
{% endif %}
</div>
<button type="submit"
class="w-full py-3 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 rounded-xl text-white font-semibold text-base transition duration-200 shadow-md hover:shadow-lg">
비밀번호 설정
</button>
</form>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,142 @@
{% extends "base.htm" %}
{% block content %}
<style>
/* "Currently:" 텍스트 숨기기 */
.help {
display: none !important;
}
/* 파일 입력 필드 스타일링 */
input[type="file"] {
border: 1px solid #4a5568;
padding: 8px;
border-radius: 4px;
background-color: #2d3748;
color: white;
width: 100%;
}
input[type="file"]::-webkit-file-upload-button {
background-color: #4299e1;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
input[type="file"]::-webkit-file-upload-button:hover {
background-color: #3182ce;
}
</style>
<div class="min-h-screen flex items-center justify-center bg-gray-900 text-white px-4">
<div class="w-full max-w-md bg-gray-800 bg-opacity-90 p-8 rounded-xl shadow-xl space-y-6">
<div class="text-center">
<h1 class="text-3xl font-bold tracking-tight text-white">신라 AMP</h1>
<p class="text-sm text-gray-400 mt-2">비밀번호 변경</p>
</div>
{% if step == 1 %}
<!-- 1단계: 전화번호 인증 -->
<form method="POST" class="space-y-4">
{% csrf_token %}
{% if error %}
<div class="text-red-400 text-sm mb-2">{{ error }}</div>
{% endif %}
{% if message %}
<div class="text-green-400 text-sm mb-2">{{ message }}</div>
{% endif %}
<div>
<label for="{{ form1.phone.id_for_label }}" class="block text-sm font-medium text-gray-300 mb-1">
{{ form1.phone.label }}
</label>
{{ form1.phone }}
{% if form1.phone.errors %}
<p class="text-sm text-red-400 mt-1">{{ form1.phone.errors.0 }}</p>
{% endif %}
</div>
{% if code_sent %}
<div>
<label for="{{ form1.verification_code.id_for_label }}" class="block text-sm font-medium text-gray-300 mb-1">
{{ form1.verification_code.label }}
</label>
{{ form1.verification_code }}
{% if form1.verification_code.errors %}
<p class="text-sm text-red-400 mt-1">{{ form1.verification_code.errors.0 }}</p>
{% endif %}
</div>
{% endif %}
<div class="flex space-x-3">
{% if not code_sent %}
<button type="submit" name="action" value="send_code"
class="flex-1 py-3 bg-blue-600 hover:bg-blue-700 rounded-xl text-white font-semibold transition">
인증번호 발송
</button>
{% else %}
<button type="submit" name="action" value="verify_code"
class="flex-1 py-3 bg-green-600 hover:bg-green-700 rounded-xl text-white font-semibold transition">
인증번호 확인
</button>
{% endif %}
</div>
</form>
{% elif step == 2 %}
<!-- 2단계: 새 비밀번호 입력 -->
<form method="POST" class="space-y-4">
{% csrf_token %}
{% if form2.errors %}
<div class="text-red-400 text-sm mb-2">
{% for field, errors in form2.errors.items %}
{% for error in errors %}
{{ error }}
{% endfor %}
{% endfor %}
</div>
{% endif %}
<div class="text-center mb-4">
<p class="text-sm text-gray-400">전화번호: {{ phone }}</p>
</div>
<div>
<label for="{{ form2.new_password1.id_for_label }}" class="block text-sm font-medium text-gray-300 mb-1">
{{ form2.new_password1.label }}
</label>
{{ form2.new_password1 }}
{% if form2.new_password1.errors %}
<p class="text-sm text-red-400 mt-1">{{ form2.new_password1.errors.0 }}</p>
{% endif %}
</div>
<div>
<label for="{{ form2.new_password2.id_for_label }}" class="block text-sm font-medium text-gray-300 mb-1">
{{ form2.new_password2.label }}
</label>
{{ form2.new_password2 }}
{% if form2.new_password2.errors %}
<p class="text-sm text-red-400 mt-1">{{ form2.new_password2.errors.0 }}</p>
{% endif %}
</div>
<button type="submit"
class="w-full py-3 bg-blue-600 hover:bg-blue-700 rounded-xl text-white font-semibold transition">
비밀번호 변경
</button>
</form>
{% endif %}
<div class="text-center text-sm">
<a href="{% url 'accounts:custom_profile_edit' %}" class="text-blue-400 hover:text-blue-500 transition">
프로필 편집으로 돌아가기
</a>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,197 @@
{% load static %}
<!DOCTYPE html>
<html lang="ko" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>비밀번호 변경 | 신라대학교 AMP 제8기</title>
<script src="https://unpkg.com/htmx.org@1.9.5"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
}
</script>
<!-- 다크모드 초기 설정 스크립트 (FOUC 방지) -->
<script>
(function() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
document.documentElement.classList.remove('dark');
}
})();
</script>
</head>
<body class="bg-gray-200 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen transition-colors duration-300">
<style>
/* "Currently:" 텍스트 숨기기 */
.help {
display: none !important;
}
/* 파일 입력 필드 스타일링 */
input[type="file"] {
border: 1px solid #4a5568;
padding: 8px;
border-radius: 4px;
background-color: #2d3748;
color: white;
width: 100%;
}
input[type="file"]::-webkit-file-upload-button {
background-color: #4299e1;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
input[type="file"]::-webkit-file-upload-button:hover {
background-color: #3182ce;
}
</style>
<div class="bg-gray-200 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen transition-colors duration-300">
<div class="max-w-5xl mx-auto px-4 py-8">
<!-- 헤더와 다크모드 토글 -->
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">신라대학교 AMP 제8기</h1>
<div class="space-x-4 text-sm">
{% if user.is_authenticated %}
<div class="flex flex-col items-end sm:items-center sm:flex-row sm:space-x-4 space-y-1 sm:space-y-0">
<!-- 데스크탑에서만 환영 메시지 표시 -->
<span class="hidden sm:block text-sm text-gray-700 dark:text-gray-200 font-semibold">
{% if user.is_superuser %}
<a href="{% url 'accounts:custom_profile_edit' %}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">{{ user.username }} 님</a>, 환영합니다!
{% else %}
<a href="{% url 'accounts:custom_profile_edit' %}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">{{ user.person.이름 }} 님</a>, 환영합니다!
{% endif %}
</span>
<!-- 모바일: 햄버거 메뉴, 데스크탑: 버튼 노출 -->
<div class="relative">
<!-- 햄버거 버튼 (모바일에서만 보임) -->
<button id="mobile-menu-button" class="sm:hidden flex items-center px-2 py-1 border rounded text-gray-700 dark:text-gray-200 border-gray-400 dark:border-gray-600 focus:outline-none" aria-label="메뉴 열기">
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
<!-- 모바일 드롭다운 메뉴 -->
<div id="mobile-menu-dropdown" class="hidden absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg z-50 border border-gray-200 dark:border-gray-700">
<!-- 모바일에서 환영 메시지 표시 -->
<div class="px-4 py-2 text-sm text-gray-700 dark:text-gray-200 font-semibold border-b border-gray-200 dark:border-gray-700">
{% if user.is_superuser %}
<a href="{% url 'accounts:custom_profile_edit' %}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">{{ user.username }} 님</a>, 환영합니다!
{% else %}
<a href="{% url 'accounts:custom_profile_edit' %}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">{{ user.person.이름 }} 님</a>, 환영합니다!
{% endif %}
</div>
<a href="{% url 'account_logout' %}" class="block px-4 py-2 text-red-500 hover:bg-gray-100 dark:hover:bg-gray-700">로그아웃</a>
</div>
<!-- 데스크탑 버튼 (sm 이상에서만 보임) -->
<div class="hidden sm:flex space-x-3 mt-1 sm:mt-0">
<a href="{% url 'account_logout' %}" class="text-red-400 hover:text-red-500">로그아웃</a>
</div>
</div>
<script>
// 햄버거 메뉴 토글 스크립트
document.addEventListener('DOMContentLoaded', function() {
const btn = document.getElementById('mobile-menu-button');
const menu = document.getElementById('mobile-menu-dropdown');
if (btn && menu) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
menu.classList.toggle('hidden');
});
// 메뉴 외부 클릭 시 닫기
document.addEventListener('click', function(e) {
if (!menu.classList.contains('hidden')) {
menu.classList.add('hidden');
}
});
// 메뉴 클릭 시 닫히지 않도록
menu.addEventListener('click', function(e) {
e.stopPropagation();
});
}
});
</script>
</div>
{% elif request.session.authenticated %}
<a href="{% url 'session_logout' %}" class="text-red-400 hover:text-red-500">로그아웃</a>
{% else %}
<a href="{% url 'account_login' %}" class="text-blue-400 hover:text-blue-500">로그인</a>
<a href="{% url 'account_signup' %}" class="text-green-400 hover:text-green-500">회원가입</a>
{% endif %}
</div>
</div>
<!-- 비밀번호 변경 폼 -->
<div class="flex justify-center">
<div class="bg-gray-800 bg-opacity-70 backdrop-blur-lg p-8 rounded-2xl shadow-2xl w-full max-w-md transition-all">
<div class="text-center mb-6">
<h2 class="text-2xl font-bold tracking-tight text-white">비밀번호 변경</h2>
<p class="text-sm text-gray-400 mt-2">현재 비밀번호를 입력하고 새 비밀번호를 설정하세요</p>
</div>
<form method="POST" class="space-y-4">
{% csrf_token %}
{% if form.errors %}
<div class="text-red-400 text-sm mb-2">
{% for field, errors in form.errors.items %}
{% for error in errors %}
{{ error }}
{% endfor %}
{% endfor %}
</div>
{% endif %}
<div>
<label for="{{ form.current_password.id_for_label }}" class="block text-sm font-medium text-gray-300 mb-1">
{{ form.current_password.label }}
</label>
{{ form.current_password }}
{% if form.current_password.errors %}
<p class="text-sm text-red-400 mt-1">{{ form.current_password.errors.0 }}</p>
{% endif %}
</div>
<div>
<label for="{{ form.new_password1.id_for_label }}" class="block text-sm font-medium text-gray-300 mb-1">
{{ form.new_password1.label }}
</label>
{{ form.new_password1 }}
{% if form.new_password1.errors %}
<p class="text-sm text-red-400 mt-1">{{ form.new_password1.errors.0 }}</p>
{% endif %}
</div>
<div>
<label for="{{ form.new_password2.id_for_label }}" class="block text-sm font-medium text-gray-300 mb-1">
{{ form.new_password2.label }}
</label>
{{ form.new_password2 }}
{% if form.new_password2.errors %}
<p class="text-sm text-red-400 mt-1">{{ form.new_password2.errors.0 }}</p>
{% endif %}
</div>
<button type="submit"
class="w-full py-3 bg-blue-600 hover:bg-blue-700 rounded-xl text-white font-semibold transition">
비밀번호 변경
</button>
</form>
<div class="text-center text-sm mt-6">
<a href="{% url 'accounts:custom_profile_edit' %}" class="text-blue-400 hover:text-blue-500 transition">
프로필 편집으로 돌아가기
</a>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="ko" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>전화번호 찾기 | 신라 AMP</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif']
}
}
}
};
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet" />
</head>
<body class="bg-gradient-to-br from-gray-900 via-gray-800 to-black text-white min-h-screen flex items-center justify-center px-4 font-sans">
<div class="bg-gray-800 bg-opacity-70 backdrop-blur-lg p-8 rounded-2xl shadow-2xl w-full max-w-sm transition-all">
<div class="text-center mb-6">
<h1 class="text-3xl font-bold tracking-tight text-white">신라 AMP</h1>
<p class="text-sm text-gray-400 mt-2">전화번호 인증을 통해 비밀번호를 재설정하세요</p>
</div>
{% if step == 1 %}
<!-- 1단계: 전화번호 인증 -->
<form method="POST" class="space-y-4">
{% csrf_token %}
{% if error %}
<div class="text-red-400 text-sm mb-2">{{ error }}</div>
{% endif %}
{% if message %}
<div class="text-green-400 text-sm mb-2">{{ message }}</div>
{% endif %}
<div>
<label for="{{ form1.phone.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form1.phone.label }}</label>
{{ form1.phone }}
{% if form1.phone.errors %}
<p class="text-sm text-red-400 mt-1">{{ form1.phone.errors.0 }}</p>
{% endif %}
</div>
{% if code_sent %}
<div>
<label for="{{ form1.verification_code.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form1.verification_code.label }}</label>
{{ form1.verification_code }}
{% if form1.verification_code.errors %}
<p class="text-sm text-red-400 mt-1">{{ form1.verification_code.errors.0 }}</p>
{% endif %}
</div>
{% endif %}
<div class="flex space-x-3">
{% if not code_sent %}
<button type="submit" name="action" value="send_code"
class="w-full py-3 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 rounded-xl text-white font-semibold text-base transition duration-200 shadow-md hover:shadow-lg">
인증번호 발송
</button>
{% else %}
<button type="submit" name="action" value="verify_code"
class="w-full py-3 bg-green-600 hover:bg-green-700 active:bg-green-800 rounded-xl text-white font-semibold text-base transition duration-200 shadow-md hover:shadow-lg">
인증번호 확인
</button>
{% endif %}
</div>
</form>
{% elif step == 2 %}
<!-- 2단계: 새 비밀번호 입력 -->
<form method="POST" class="space-y-4">
{% csrf_token %}
{% if form2.errors %}
<div class="text-red-400 text-sm mb-2">
{% for field, errors in form2.errors.items %}
{% for error in errors %}
{{ error }}
{% endfor %}
{% endfor %}
</div>
{% endif %}
<div class="text-center mb-4">
<p class="text-sm text-gray-400">전화번호: {{ phone }}</p>
</div>
<div>
<label for="{{ form2.new_password1.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form2.new_password1.label }}</label>
{{ form2.new_password1 }}
{% if form2.new_password1.errors %}
<p class="text-sm text-red-400 mt-1">{{ form2.new_password1.errors.0 }}</p>
{% endif %}
</div>
<div>
<label for="{{ form2.new_password2.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form2.new_password2.label }}</label>
{{ form2.new_password2 }}
{% if form2.new_password2.errors %}
<p class="text-sm text-red-400 mt-1">{{ form2.new_password2.errors.0 }}</p>
{% endif %}
</div>
<button type="submit"
class="w-full py-3 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 rounded-xl text-white font-semibold text-base transition duration-200 shadow-md hover:shadow-lg">
비밀번호 재설정
</button>
</form>
{% endif %}
<div class="mt-6 text-center text-sm">
<a href="{% url 'account_login' %}" class="text-blue-400 hover:text-blue-500 transition">
로그인으로 돌아가기
</a>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,350 @@
{% load static %}
<!DOCTYPE html>
<html lang="ko" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>프로필 수정 | 신라대학교 AMP 제8기</title>
<script src="https://unpkg.com/htmx.org@1.9.5"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
}
</script>
<!-- 다크모드 초기 설정 스크립트 (FOUC 방지) -->
<script>
(function() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
document.documentElement.classList.remove('dark');
}
})();
</script>
</head>
<body class="bg-gray-200 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen transition-colors duration-300">
<style>
/* "Currently:" 텍스트 숨기기 */
.help {
display: none !important;
}
/* 파일 입력 필드 스타일링 */
input[type="file"] {
border: 1px solid #4a5568;
padding: 8px;
border-radius: 4px;
background-color: #2d3748;
color: white;
width: 100%;
}
input[type="file"]::-webkit-file-upload-button {
background-color: #4299e1;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
input[type="file"]::-webkit-file-upload-button:hover {
background-color: #3182ce;
}
</style>
<div class="bg-gray-200 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen transition-colors duration-300">
<div class="max-w-5xl mx-auto px-4 py-8">
<!-- 헤더와 다크모드 토글 -->
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">신라대학교 AMP 제8기</h1>
<div class="space-x-4 text-sm">
{% if user.is_authenticated %}
<div class="flex flex-col items-end sm:items-center sm:flex-row sm:space-x-4 space-y-1 sm:space-y-0">
<!-- 데스크탑에서만 환영 메시지 표시 -->
<span class="hidden sm:block text-sm text-gray-700 dark:text-gray-200 font-semibold">
{% if user.is_superuser %}
<a href="{% url 'accounts:custom_profile_edit' %}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">{{ user.username }} 님</a>, 환영합니다!
{% else %}
<a href="{% url 'accounts:custom_profile_edit' %}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">{{ user.person.이름 }} 님</a>, 환영합니다!
{% endif %}
</span>
<!-- 모바일: 햄버거 메뉴, 데스크탑: 버튼 노출 -->
<div class="relative">
<!-- 모바일: 다크모드 토글 버튼과 햄버거 버튼을 가로로 배치 -->
<div class="sm:hidden flex items-center space-x-2">
<!-- 모바일: 다크모드 토글 버튼 (햄버거 버튼 왼쪽) -->
<button id="theme-toggle-mobile" class="p-2 rounded-lg bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors" aria-label="테마 변경">
<!-- 라이트 모드 아이콘 (다크모드일 때 보임) -->
<svg id="theme-toggle-light-icon-mobile" class="w-5 h-5 text-gray-800 dark:text-gray-200" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"></path>
</svg>
<!-- 다크 모드 아이콘 (라이트모드일 때 보임) -->
<svg id="theme-toggle-dark-icon-mobile" class="w-5 h-5 text-gray-800 dark:text-gray-200 hidden" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
</button>
<!-- 햄버거 버튼 (모바일에서만 보임) -->
<button id="mobile-menu-button" class="flex items-center px-2 py-1 border rounded text-gray-700 dark:text-gray-200 border-gray-400 dark:border-gray-600 focus:outline-none" aria-label="메뉴 열기">
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
</div>
<!-- 모바일 드롭다운 메뉴 -->
<div id="mobile-menu-dropdown" class="hidden absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg z-50 border border-gray-200 dark:border-gray-700">
<!-- 모바일에서 환영 메시지 표시 -->
<div class="px-4 py-2 text-sm text-gray-700 dark:text-gray-200 font-semibold border-b border-gray-200 dark:border-gray-700">
{% if user.is_superuser %}
<a href="{% url 'accounts:custom_profile_edit' %}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">{{ user.username }} 님</a>, 환영합니다!
{% else %}
<a href="{% url 'accounts:custom_profile_edit' %}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">{{ user.person.이름 }} 님</a>, 환영합니다!
{% endif %}
</div>
<a href="{% url 'account_logout' %}" class="block px-4 py-2 text-red-500 hover:bg-gray-100 dark:hover:bg-gray-700">로그아웃</a>
</div>
<!-- 데스크탑 버튼 (sm 이상에서만 보임) -->
<div class="hidden sm:flex items-center space-x-3 mt-1 sm:mt-0">
<a href="{% url 'account_logout' %}" class="text-red-400 hover:text-red-500">로그아웃</a>
<!-- 데스크탑: 다크모드 토글 버튼 (로그아웃 오른쪽) -->
<button id="theme-toggle" class="p-2 rounded-lg bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors" aria-label="테마 변경">
<!-- 라이트 모드 아이콘 (다크모드일 때 보임) -->
<svg id="theme-toggle-light-icon" class="w-5 h-5 text-gray-800 dark:text-gray-200" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"></path>
</svg>
<!-- 다크 모드 아이콘 (라이트모드일 때 보임) -->
<svg id="theme-toggle-dark-icon" class="w-5 h-5 text-gray-800 dark:text-gray-200 hidden" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
</button>
</div>
</div>
<script>
// 햄버거 메뉴 토글 스크립트
document.addEventListener('DOMContentLoaded', function() {
const btn = document.getElementById('mobile-menu-button');
const menu = document.getElementById('mobile-menu-dropdown');
if (btn && menu) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
menu.classList.toggle('hidden');
});
// 메뉴 외부 클릭 시 닫기
document.addEventListener('click', function(e) {
if (!menu.classList.contains('hidden')) {
menu.classList.add('hidden');
}
});
// 메뉴 클릭 시 닫히지 않도록
menu.addEventListener('click', function(e) {
e.stopPropagation();
});
}
});
</script>
</div>
{% elif request.session.authenticated %}
<a href="{% url 'session_logout' %}" class="text-red-400 hover:text-red-500">로그아웃</a>
{% else %}
<a href="{% url 'account_login' %}" class="text-blue-400 hover:text-blue-500">로그인</a>
<a href="{% url 'account_signup' %}" class="text-green-400 hover:text-green-500">회원가입</a>
{% endif %}
</div>
</div>
<!-- 프로필 수정 폼 -->
<div class="flex justify-center">
<div class="bg-gray-800 bg-opacity-70 backdrop-blur-lg p-8 rounded-2xl shadow-2xl w-full max-w-md transition-all">
<div class="text-center mb-6">
<h2 class="text-2xl font-bold tracking-tight text-white">프로필 수정</h2>
<p class="text-sm text-gray-400 mt-2">개인 정보를 수정하세요</p>
</div>
{% if messages %}
{% for message in messages %}
<div class="p-4 rounded-lg {% if message.tags == 'success' %}bg-green-600{% else %}bg-red-600{% endif %} text-white mb-4">
{{ message }}
</div>
{% endfor %}
{% endif %}
<form method="POST" enctype="multipart/form-data" class="space-y-4">
{% csrf_token %}
{% if form.errors %}
<div class="text-red-400 text-sm mb-2">
{% for field, errors in form.errors.items %}
{% for error in errors %}
{{ error }}
{% endfor %}
{% endfor %}
</div>
{% endif %}
<!-- 편집 불가능한 필드들 (표시만) -->
<div>
<label class="block mb-1 text-sm text-gray-300">이름</label>
{{ form.full_name }}
</div>
<div>
<label class="block mb-1 text-sm text-gray-300">전화번호</label>
<input type="text" value="{{ user.username }}" readonly
class="w-full px-4 py-3 rounded-xl bg-gray-600 bg-opacity-80 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition">
</div>
{% if form.instance.생년월일 %}
<div>
<label class="block mb-1 text-sm text-gray-300">생년월일</label>
<input type="text" value="{{ form.instance.생년월일|date:'Y-m-d' }}" readonly
class="w-full px-4 py-3 rounded-xl bg-gray-600 bg-opacity-80 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition">
</div>
{% endif %}
{% if form.instance.TITLE %}
<div>
<label class="block mb-1 text-sm text-gray-300">TITLE</label>
<input type="text" value="{{ form.instance.TITLE }}" readonly
class="w-full px-4 py-3 rounded-xl bg-gray-600 bg-opacity-80 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 transition">
</div>
{% endif %}
<!-- 편집 가능한 필드들 -->
<div>
<label for="{{ form.소속.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form.소속.label }}</label>
{{ form.소속 }}
</div>
<div>
<label for="{{ form.직책.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form.직책.label }}</label>
{{ form.직책 }}
</div>
<div>
<label for="{{ form.주소.id_for_label }}" class="block mb-1 text-sm text-gray-300">{{ form.주소.label }}</label>
{{ form.주소 }}
</div>
<div>
<label for="{{ form.사진.id_for_label }}" class="block mb-1 text-sm text-gray-300">프로필 사진</label>
{{ form.사진 }}
{% if form.instance.사진 and form.instance.사진.url %}
<div class="mt-2">
<img id="profile-preview" src="{{ form.instance.사진.url }}" alt="프로필 사진 미리보기" class="w-24 h-24 rounded-full object-cover border-2 border-gray-500" />
</div>
{% else %}
<div class="mt-2">
<img id="profile-preview" src="/static/B_main/images/default_user.png" alt="프로필 사진 미리보기" class="w-24 h-24 rounded-full object-cover border-2 border-gray-500" />
</div>
{% endif %}
</div>
<!-- 키워드 섹션 -->
<div class="border-t border-gray-600 pt-4">
<h3 class="text-lg font-semibold text-blue-400 mb-3">검색 키워드</h3>
<p class="text-sm text-gray-400 mb-4">다른 사람들이 당신을 찾을 수 있도록 키워드를 설정하세요</p>
<div class="mb-3">
{{ form.keyword1 }}
</div>
</div>
<button type="submit"
class="w-full py-3 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 rounded-xl text-white font-semibold text-base transition duration-200 shadow-md hover:shadow-lg">
프로필 저장
</button>
</form>
<!-- 비밀번호 변경 섹션 -->
<div class="mt-6 pt-6 border-t border-gray-600">
<div class="text-center">
<div class="space-y-3">
<a href="{% url 'accounts:password_change_logged_in' %}"
class="block w-full px-6 py-2 bg-orange-600 hover:bg-orange-700 rounded-lg text-white font-medium text-sm transition duration-200 shadow-md hover:shadow-lg">
비밀번호 변경
</a>
</div>
</div>
</div>
<div class="mt-6 text-center text-sm">
<a href="{% url 'main' %}" class="text-blue-400 hover:text-blue-500 transition">
메인으로 돌아가기
</a>
</div>
</div>
</div>
</div>
</div>
<script>
// 사진 업로드 시 미리보기
document.querySelector('input[type=file][name$=사진]').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(ev) {
document.getElementById('profile-preview').src = ev.target.result;
};
reader.readAsDataURL(file);
}
});
// 다크모드 토글 스크립트
const themeToggle = document.getElementById('theme-toggle');
const themeToggleMobile = document.getElementById('theme-toggle-mobile');
const lightIcon = document.getElementById('theme-toggle-light-icon');
const darkIcon = document.getElementById('theme-toggle-dark-icon');
const lightIconMobile = document.getElementById('theme-toggle-light-icon-mobile');
const darkIconMobile = document.getElementById('theme-toggle-dark-icon-mobile');
// 아이콘 초기 설정 함수
function updateIcons() {
const isDark = document.documentElement.classList.contains('dark');
if (isDark) {
// 다크모드일 때
if (lightIcon) lightIcon.classList.remove('hidden');
if (darkIcon) darkIcon.classList.add('hidden');
if (lightIconMobile) lightIconMobile.classList.remove('hidden');
if (darkIconMobile) darkIconMobile.classList.add('hidden');
} else {
// 라이트모드일 때
if (lightIcon) lightIcon.classList.add('hidden');
if (darkIcon) darkIcon.classList.remove('hidden');
if (lightIconMobile) lightIconMobile.classList.add('hidden');
if (darkIconMobile) darkIconMobile.classList.remove('hidden');
}
}
// 초기 아이콘 설정
updateIcons();
// 테마 토글 함수
function toggleTheme() {
const isDark = document.documentElement.classList.contains('dark');
if (isDark) {
// 라이트 모드로 전환
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
} else {
// 다크 모드로 전환
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
}
updateIcons();
}
// 데스크탑 토글 버튼 클릭 이벤트
if (themeToggle) {
themeToggle.addEventListener('click', toggleTheme);
}
// 모바일 토글 버튼 클릭 이벤트
if (themeToggleMobile) {
themeToggleMobile.addEventListener('click', toggleTheme);
}
</script>
</body>
</html>

View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="ko" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}신라 AMP{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif']
}
}
}
};
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet" />
</head>
<body class="bg-gradient-to-br from-gray-900 via-gray-800 to-black text-white min-h-screen font-sans">
<!-- 상단 헤더 -->
<header class="p-4 border-b border-gray-700 flex justify-between items-center">
<h1 class="text-xl font-bold">신라대학교 AMP 제8기</h1>
{% if user.is_authenticated %}
<div class="text-sm text-gray-300">
{{ user.email }}님 |
<a href="{% url 'account_logout' %}" class="text-red-400 hover:text-red-500">로그아웃</a>
</div>
{% else %}
<div class="text-sm space-x-4">
<a href="{% url 'account_login' %}" class="text-blue-400 hover:text-blue-500">로그인</a>
<a href="{% url 'account_signup' %}" class="text-green-400 hover:text-green-500">회원가입</a>
</div>
{% endif %}
</header>
<!-- 콘텐츠 블럭 -->
<main class="p-6">
{% block content %}{% endblock %}
</main>
</body>
</html>

View File

View File

@ -0,0 +1,7 @@
from django import template
register = template.Library()
@register.filter(name='add_class')
def add_class(field, css):
return field.as_widget(attrs={"class": css})

3
C_accounts/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

13
C_accounts/urls.py Normal file
View File

@ -0,0 +1,13 @@
# C_accounts/urls.py
from django.urls import path
from . import views
app_name = 'accounts'
urlpatterns = [
path('profile_edit/', views.profile_edit, name='custom_profile_edit'),
path('password_change/', views.password_change, name='password_change'),
path('password_reset/', views.password_reset, name='password_reset'),
path('password_change_logged_in/', views.password_change_logged_in, name='password_change_logged_in'),
path('force_password_set/', views.force_password_set, name='force_password_set'),
]

271
C_accounts/views.py Normal file
View File

@ -0,0 +1,271 @@
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.http import JsonResponse
from .forms import (
ProfileFullEditForm, PasswordChangeStep1Form, PasswordChangeStep2Form,
PasswordResetStep1Form, PasswordChangeLoggedInForm, ForcePasswordSetForm
)
from B_main.models import Person
import random
User = get_user_model()
@login_required
def profile_edit(request):
"""프로필 편집 뷰"""
# 현재 사용자의 Person 인스턴스 가져오기
try:
person = Person.objects.get(user=request.user)
except Person.DoesNotExist:
# Person 인스턴스가 없으면 새로 생성
person = Person.objects.create(user=request.user)
if request.method == 'POST':
form = ProfileFullEditForm(request.POST, request.FILES, user=request.user, instance=person)
if form.is_valid():
form.save()
messages.success(request, '프로필이 성공적으로 업데이트되었습니다.')
return redirect('accounts:custom_profile_edit')
else:
form = ProfileFullEditForm(user=request.user, instance=person)
return render(request, 'C_accounts/profile_edit.html', {'form': form})
@login_required
def password_change(request):
"""비밀번호 변경 뷰 (2단계 프로세스)"""
# 세션 초기화
if 'password_change_step' not in request.session:
request.session['password_change_step'] = 1
request.session['password_change_code'] = None
request.session['password_change_phone'] = None
request.session['password_change_verified'] = False
step = request.session.get('password_change_step', 1)
code_sent = request.session.get('password_change_code') is not None
verified = request.session.get('password_change_verified', False)
phone = request.session.get('password_change_phone')
error = None
message = None
if step == 1:
if request.method == 'POST':
action = request.POST.get('action')
if action == 'send_code':
form1 = PasswordChangeStep1Form(request.POST, user=request.user)
if form1.is_valid():
phone = form1.cleaned_data['phone']
# 인증번호 생성 (실제로는 SMS 발송)
verification_code = str(random.randint(100000, 999999))
print(f"[DEBUG] 인증번호: {verification_code}") # 실제로는 SMS 발송
request.session['password_change_code'] = verification_code
request.session['password_change_phone'] = phone
request.session['password_change_step'] = 1
message = '인증번호가 발송되었습니다.'
code_sent = True
else:
error = '전화번호를 확인해주세요.'
elif action == 'verify_code':
form1 = PasswordChangeStep1Form(request.POST, user=request.user)
if form1.is_valid():
input_code = form1.cleaned_data['verification_code']
stored_code = request.session.get('password_change_code')
if input_code == stored_code:
request.session['password_change_verified'] = True
request.session['password_change_step'] = 2
return redirect('accounts:password_change')
else:
error = '인증번호가 일치하지 않습니다.'
else:
error = '인증번호를 확인해주세요.'
else:
form1 = PasswordChangeStep1Form(user=request.user)
return render(request, 'C_accounts/password_change.html', {
'step': 1, 'form1': form1, 'code_sent': code_sent, 'error': error, 'message': message
})
elif step == 2 and verified and phone:
if request.method == 'POST':
form2 = PasswordChangeStep2Form(request.POST)
if form2.is_valid():
new_password = form2.cleaned_data['new_password1']
request.user.set_password(new_password)
request.user.save()
# 세션 정리
del request.session['password_change_step']
del request.session['password_change_code']
del request.session['password_change_phone']
del request.session['password_change_verified']
messages.success(request, '비밀번호가 성공적으로 변경되었습니다.')
return redirect('accounts:custom_profile_edit')
else:
return render(request, 'C_accounts/password_change.html', {
'step': 2, 'form2': form2, 'phone': phone
})
else:
form2 = PasswordChangeStep2Form()
return render(request, 'C_accounts/password_change.html', {
'step': 2, 'form2': form2, 'phone': phone
})
# 기본: 1단계로 초기화
request.session['password_change_step'] = 1
request.session['password_change_verified'] = False
return redirect('accounts:password_change')
# 모드1: 비밀번호 찾기 (로그인하지 않은 상태)
def password_reset(request):
"""비밀번호 찾기 뷰"""
# 세션 초기화
if 'password_reset_step' not in request.session:
request.session['password_reset_step'] = 1
request.session['password_reset_code'] = None
request.session['password_reset_phone'] = None
request.session['password_reset_verified'] = False
step = request.session.get('password_reset_step', 1)
code_sent = request.session.get('password_reset_code') is not None
verified = request.session.get('password_reset_verified', False)
phone = request.session.get('password_reset_phone')
error = None
message = None
if step == 1:
if request.method == 'POST':
action = request.POST.get('action')
if action == 'send_code':
form1 = PasswordResetStep1Form(request.POST)
if form1.is_valid():
phone = form1.cleaned_data['phone']
# 인증번호 생성 (실제로는 SMS 발송)
verification_code = str(random.randint(100000, 999999))
print(f"[DEBUG] 비밀번호 찾기 인증번호: {verification_code}") # 실제로는 SMS 발송
request.session['password_reset_code'] = verification_code
request.session['password_reset_phone'] = phone
request.session['password_reset_step'] = 1
message = '인증번호가 발송되었습니다.'
code_sent = True
else:
error = '전화번호를 확인해주세요.'
elif action == 'verify_code':
form1 = PasswordResetStep1Form(request.POST)
if form1.is_valid():
input_code = form1.cleaned_data['verification_code']
stored_code = request.session.get('password_reset_code')
if input_code == stored_code:
request.session['password_reset_verified'] = True
request.session['password_reset_step'] = 2
return redirect('accounts:password_reset')
else:
error = '인증번호가 일치하지 않습니다.'
else:
error = '인증번호를 확인해주세요.'
else:
form1 = PasswordResetStep1Form()
return render(request, 'C_accounts/password_reset.html', {
'step': 1, 'form1': form1, 'code_sent': code_sent, 'error': error, 'message': message
})
elif step == 2 and verified and phone:
if request.method == 'POST':
form2 = ForcePasswordSetForm(request.POST)
if form2.is_valid():
new_password = form2.cleaned_data['new_password1']
# 해당 전화번호의 사용자 찾기
try:
user = User.objects.get(username=phone)
user.set_password(new_password)
user.save()
# 세션 정리
del request.session['password_reset_step']
del request.session['password_reset_code']
del request.session['password_reset_phone']
del request.session['password_reset_verified']
messages.success(request, '비밀번호가 성공적으로 재설정되었습니다. 새 비밀번호로 로그인해주세요.')
return redirect('account_login')
except User.DoesNotExist:
error = '사용자를 찾을 수 없습니다.'
else:
return render(request, 'C_accounts/password_reset.html', {
'step': 2, 'form2': form2, 'phone': phone
})
else:
form2 = ForcePasswordSetForm()
return render(request, 'C_accounts/password_reset.html', {
'step': 2, 'form2': form2, 'phone': phone
})
# 기본: 1단계로 초기화
request.session['password_reset_step'] = 1
request.session['password_reset_verified'] = False
return redirect('accounts:password_reset')
# 모드2: 로그인 상태에서 비밀번호 변경
@login_required
def password_change_logged_in(request):
"""로그인 상태에서 비밀번호 변경 뷰"""
if request.method == 'POST':
form = PasswordChangeLoggedInForm(request.POST, user=request.user)
if form.is_valid():
new_password = form.cleaned_data['new_password1']
request.user.set_password(new_password)
request.user.save()
messages.success(request, '비밀번호가 성공적으로 변경되었습니다.')
return redirect('accounts:custom_profile_edit')
else:
form = PasswordChangeLoggedInForm(user=request.user)
return render(request, 'C_accounts/password_change_logged_in.html', {'form': form})
# 모드3: 강제 비밀번호 설정
@login_required
def force_password_set(request):
"""강제 비밀번호 설정 뷰"""
# 현재 사용자의 Person 인스턴스 확인
try:
person = Person.objects.get(user=request.user)
if not person.비밀번호설정필요:
return redirect('main')
except Person.DoesNotExist:
return redirect('main')
if request.method == 'POST':
form = ForcePasswordSetForm(request.POST)
if form.is_valid():
new_password = form.cleaned_data['new_password1']
request.user.set_password(new_password)
request.user.save()
# 비밀번호 설정 필요 플래그 해제
person.비밀번호설정필요 = False
person.save()
# 로그아웃 처리
from django.contrib.auth import logout
logout(request)
# 로그아웃 후 세션에 메시지 저장 (로그인 페이지에서 표시)
request.session['password_set_message'] = '비밀번호가 성공적으로 설정되었습니다. 새 비밀번호로 로그인해주세요.'
return redirect('account_login')
else:
form = ForcePasswordSetForm()
return render(request, 'C_accounts/force_password_set.html', {'form': form})

BIN
db.sqlite3 Normal file

Binary file not shown.

22
manage.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'A_core.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Some files were not shown because too many files have changed in this diff Show More