diff --git a/app.py b/app.py index a21bd0e..665d28d 100644 --- a/app.py +++ b/app.py @@ -1,14 +1,14 @@ # Hello World Flask 애플리케이션 예제 -from flask import Flask # Flask 모듈 임포트 +from flask import Flask, render_template # Flask 모듈 임포트 # Flask는 파이썬으로 작성된 웹 프레임워크로, 웹 애플리케이션을 쉽게 만들 수 있도록 도와줍니다. app = Flask(__name__) # Flask 애플리케이션 객체 생성 @app.route("/") # 루트 URL에 대한 라우팅 설정 -def hello(): - return "Hello, World!" # 브라우저에 출력될 텍스트 +def home(): + return render_template("index_html") # 브라우저에 출력될 텍스트 if __name__ == "__main__": app.run(debug=True) # 개발 서버 실행 (디버그 모드) diff --git a/clubsite/__pycache__/config.cpython-313.pyc b/clubsite/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..3145d62 Binary files /dev/null and b/clubsite/__pycache__/config.cpython-313.pyc differ diff --git a/clubsite/app/__init__.py b/clubsite/app/__init__.py new file mode 100644 index 0000000..76fdd33 --- /dev/null +++ b/clubsite/app/__init__.py @@ -0,0 +1,76 @@ +# app/__init__.py +from flask import Flask, render_template +from flask_login import LoginManager, current_user, login_required +from config import Config + +login_manager = LoginManager() + +@login_manager.user_loader +def load_user(user_id): + # 함수 내부에서 import하여 순환 import 방지 + from app.models import User + return User.query.get(int(user_id)) + +def create_app(): + app = Flask(__name__) + app.config.from_object(Config) + + # DB 초기화 (늦은 import) + from app.models import db + db.init_app(app) + + # 로그인 매니저 초기화 + login_manager.init_app(app) + login_manager.login_view = 'auth.login' + login_manager.login_message = '로그인이 필요합니다.' + login_manager.login_message_category = 'info' + + @app.context_processor + def inject_user(): + return { + 'current_user': current_user, + 'is_admin': current_user.is_authenticated and current_user.is_admin_user() + } + + # 블루프린트 등록 (늦은 import) + from app.routes.auth import auth_bp + from app.routes.admin import admin_bp + + app.register_blueprint(auth_bp, url_prefix='/auth') + app.register_blueprint(admin_bp, url_prefix='/admin') + + # 메인 라우트 + from flask import Blueprint + main_bp = Blueprint('main', __name__) + + @main_bp.route('/') + def index(): + return render_template('index0.html', title='동아리 홈페이지') + + @main_bp.route('/dashboard') + @login_required + def dashboard(): + user_teams = current_user.get_teams() + return render_template('index.html', + title='대시보드', + teams=user_teams) + + + app.register_blueprint(main_bp) + + # 에러 핸들러 + @app.errorhandler(403) + def forbidden(error): + return render_template('errors/403.html'), 403 + + @app.errorhandler(404) + def not_found(error): + return render_template('errors/404.html'), 404 + + @app.errorhandler(500) + def internal_error(error): + from app.models import db # 함수 내부에서 import + db.session.rollback() + return render_template('errors/500.html'), 500 + + return app \ No newline at end of file diff --git a/clubsite/app/__pycache__/__init__.cpython-313.pyc b/clubsite/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..2f143fb Binary files /dev/null and b/clubsite/app/__pycache__/__init__.cpython-313.pyc differ diff --git a/clubsite/app/__pycache__/models.cpython-313.pyc b/clubsite/app/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000..cb70474 Binary files /dev/null and b/clubsite/app/__pycache__/models.cpython-313.pyc differ diff --git a/clubsite/app/decorators.py b/clubsite/app/decorators.py new file mode 100644 index 0000000..1f4bede --- /dev/null +++ b/clubsite/app/decorators.py @@ -0,0 +1,84 @@ +# app/decorators.py +from functools import wraps +from flask import abort, redirect, url_for, request, flash +from flask_login import current_user, login_required + +def admin_required(f): + """관리자 권한이 필요한 페이지에 사용하는 데코레이터""" + @wraps(f) + @login_required + def decorated_function(*args, **kwargs): + if not current_user.is_admin_user(): + flash('관리자 권한이 필요합니다.', 'error') + abort(403) + return f(*args, **kwargs) + return decorated_function + +def group_access_required(f): + """그룹 상세 정보 접근 시 권한 확인 데코레이터""" + @wraps(f) + @login_required + def decorated_function(*args, **kwargs): + # 함수 호출 시점에 import하여 순환 import 방지 + from app.models import Group + + group_id = kwargs.get('group_id') or request.view_args.get('group_id') + if not group_id: + abort(404) + + group = Group.query.get_or_404(group_id) + + # 관리자는 모든 그룹에 접근 가능 + if current_user.is_admin_user(): + return f(*args, **kwargs) + + # 해당 그룹의 멤버인지 확인 + if not group.has_member(current_user.id): + flash('해당 그룹의 정보에 접근할 권한이 없습니다.', 'error') + abort(403) + + return f(*args, **kwargs) + return decorated_function + +def member_access_required(f): + """멤버 개인정보 접근 시 권한 확인 데코레이터""" + @wraps(f) + @login_required + def decorated_function(*args, **kwargs): + from app.models import Member + + member_id = kwargs.get('member_id') or request.view_args.get('member_id') + if not member_id: + abort(404) + + member = Member.query.get_or_404(member_id) + + # 관리자는 모든 멤버 정보에 접근 가능 + if current_user.is_admin_user(): + return f(*args, **kwargs) + + # 같은 그룹 멤버인지 확인 + user_member = current_user.get_member_info() + if not user_member or user_member.group_id != member.group_id: + flash('해당 멤버의 정보에 접근할 권한이 없습니다.', 'error') + abort(403) + + return f(*args, **kwargs) + return decorated_function + +def own_profile_or_admin_required(f): + """자신의 프로필이거나 관리자만 접근 가능한 데코레이터""" + @wraps(f) + @login_required + def decorated_function(*args, **kwargs): + user_id = kwargs.get('user_id') or request.view_args.get('user_id') + if not user_id: + abort(404) + + # 관리자이거나 자신의 프로필인 경우 + if current_user.is_admin_user() or current_user.id == int(user_id): + return f(*args, **kwargs) + + flash('접근 권한이 없습니다.', 'error') + abort(403) + return decorated_function \ No newline at end of file diff --git a/clubsite/app/models.py b/clubsite/app/models.py new file mode 100644 index 0000000..65dd0da --- /dev/null +++ b/clubsite/app/models.py @@ -0,0 +1,81 @@ +# app/models.py +from flask_sqlalchemy import SQLAlchemy +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash +from datetime import datetime + +db = SQLAlchemy() + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(150), unique=True, nullable=False) + password_hash = db.Column(db.String(150), nullable=False) + is_admin = db.Column(db.Boolean, default=False) + email = db.Column(db.String(150), unique=True, nullable=False) + birthdate = db.Column(db.Date, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + # User와 Member 관계 설정 + member = db.relationship('Member', backref='user', uselist=False) + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + + def is_admin_user(self): + """관리자인지 확인""" + return self.is_admin + + def get_member_info(self): + """해당 사용자의 Member 정보 반환""" + return Member.query.filter_by(user_id=self.id).first() + + def get_accessible_groups(self): + """접근 가능한 그룹들 반환""" + if self.is_admin: + return Group.query.all() + else: + member = self.get_member_info() + if member: + return [member.group] + return [] + + def get_teams(self): + """사용자가 속한 팀들 반환 (auth.py에서 사용)""" + return self.get_accessible_groups() + +class Category(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + groups = db.relationship('Group', backref='category', lazy=True) + + def __repr__(self): + return f'' + +class Group(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=False) + members = db.relationship('Member', backref='group', lazy=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + def get_member_count(self): + """그룹 멤버 수 반환""" + return len(self.members) + + def has_member(self, user_id): + """특정 사용자가 이 그룹의 멤버인지 확인""" + return any(member.user_id == user_id for member in self.members if member.user_id) + + def __repr__(self): + return f'' + +class Member(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + department = db.Column(db.String(150)) + blog_url = db.Column(db.String(200)) + group_id = db.Column(db.Integer, db.ForeignKey('group.id'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) \ No newline at end of file diff --git a/clubsite/app/routes/__pycache__/admin.cpython-313.pyc b/clubsite/app/routes/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000..eaa28ac Binary files /dev/null and b/clubsite/app/routes/__pycache__/admin.cpython-313.pyc differ diff --git a/clubsite/app/routes/__pycache__/auth.cpython-313.pyc b/clubsite/app/routes/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000..f3873fd Binary files /dev/null and b/clubsite/app/routes/__pycache__/auth.cpython-313.pyc differ diff --git a/clubsite/app/routes/__pycache__/main.cpython-313.pyc b/clubsite/app/routes/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..a590084 Binary files /dev/null and b/clubsite/app/routes/__pycache__/main.cpython-313.pyc differ diff --git a/clubsite/app/routes/admin.py b/clubsite/app/routes/admin.py new file mode 100644 index 0000000..7bd02ee --- /dev/null +++ b/clubsite/app/routes/admin.py @@ -0,0 +1,237 @@ +# app/routes/admin.py +from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify +from flask_login import current_user +from sqlalchemy import func + +admin_bp = Blueprint('admin', __name__) + +@admin_bp.route('/dashboard') +def dashboard(): + """관리자 대시보드""" + # 함수 내부에서 import하여 순환 import 방지 + from app.models import db, User, Group, Member, Category + from app.decorators import admin_required + + # 데코레이터 수동 적용 + if not current_user.is_authenticated or not current_user.is_admin_user(): + flash('관리자 권한이 필요합니다.', 'error') + return redirect(url_for('auth.login')) + + # 통계 정보 수집 + total_users = User.query.count() + total_groups = Group.query.count() + total_members = Member.query.count() + + # 카테고리별 그룹 수 + group_stats = (db.session.query(Category.name, func.count(Group.id).label('count')) + .join(Group, Category.id == Group.category_id) + .group_by(Category.name).all()) + + # 최근 가입한 사용자들 + recent_users = User.query.order_by(User.created_at.desc()).limit(5).all() + + return render_template('admin/dashboard.html', + title='관리자 대시보드', + total_users=total_users, + total_groups=total_groups, + total_members=total_members, + group_stats=group_stats, + recent_users=recent_users) + +@admin_bp.route('/users') +def manage_users(): + """사용자 관리 페이지""" + from app.models import User + + if not current_user.is_authenticated or not current_user.is_admin_user(): + flash('관리자 권한이 필요합니다.', 'error') + return redirect(url_for('auth.login')) + + page = request.args.get('page', 1, type=int) + per_page = 20 + + users = User.query.paginate( + page=page, per_page=per_page, error_out=False + ) + + return render_template('admin/users.html', + title='사용자 관리', + users=users) + +@admin_bp.route('/users//admin-toggle', methods=['POST']) +def toggle_user_admin(user_id): + """사용자 관리자 권한 토글""" + from app.models import db, User + + if not current_user.is_authenticated or not current_user.is_admin_user(): + flash('관리자 권한이 필요합니다.', 'error') + return redirect(url_for('auth.login')) + + user = User.query.get_or_404(user_id) + + # 자기 자신의 권한은 변경할 수 없음 + if user.id == current_user.id: + flash('자신의 권한은 변경할 수 없습니다.', 'error') + return redirect(url_for('admin.manage_users')) + + old_status = '관리자' if user.is_admin else '일반사용자' + user.is_admin = not user.is_admin + new_status = '관리자' if user.is_admin else '일반사용자' + + db.session.commit() + + flash(f'{user.name}의 권한이 {old_status}에서 {new_status}로 변경되었습니다.', 'success') + return redirect(url_for('admin.manage_users')) + +@admin_bp.route('/users//delete', methods=['POST']) +def delete_user(user_id): + """사용자 삭제""" + from app.models import db, User + + if not current_user.is_authenticated or not current_user.is_admin_user(): + flash('관리자 권한이 필요합니다.', 'error') + return redirect(url_for('auth.login')) + + user = User.query.get_or_404(user_id) + + # 자기 자신은 삭제할 수 없음 + if user.id == current_user.id: + flash('자신의 계정은 삭제할 수 없습니다.', 'error') + return redirect(url_for('admin.manage_users')) + + username = user.name + + db.session.delete(user) + db.session.commit() + + flash(f'{username} 사용자가 삭제되었습니다.', 'info') + return redirect(url_for('admin.manage_users')) + +@admin_bp.route('/groups') +def manage_groups(): + """그룹 관리 페이지""" + from app.models import Group + + if not current_user.is_authenticated or not current_user.is_admin_user(): + flash('관리자 권한이 필요합니다.', 'error') + return redirect(url_for('auth.login')) + + groups = Group.query.all() + + return render_template('admin/groups.html', + title='그룹 관리', + groups=groups) + +@admin_bp.route('/groups/') +def group_admin_detail(group_id): + """관리자용 그룹 상세 정보""" + from app.models import Group, Member + + if not current_user.is_authenticated or not current_user.is_admin_user(): + flash('관리자 권한이 필요합니다.', 'error') + return redirect(url_for('auth.login')) + + group = Group.query.get_or_404(group_id) + members = Member.query.filter_by(group_id=group_id).all() + + return render_template('admin/group_detail.html', + title=f'{group.name} 그룹 관리', + group=group, + members=members) + +@admin_bp.route('/groups/create', methods=['GET', 'POST']) +def create_group(): + """새 그룹 생성""" + from app.models import db, Group, Category + + if not current_user.is_authenticated or not current_user.is_admin_user(): + flash('관리자 권한이 필요합니다.', 'error') + return redirect(url_for('login')) + + if request.method == 'POST': + name = request.form.get('name') + category_id = request.form.get('category_id') + + if not name or not category_id: + flash('그룹 이름과 카테고리를 입력해주세요.', 'error') + categories = Category.query.all() + return render_template('admin/create_group.html', + title='새 그룹 생성', + categories=categories) + + # 중복 확인 + if Group.query.filter_by(name=name).first(): + flash('이미 존재하는 그룹명입니다.', 'error') + categories = Category.query.all() + return render_template('admin/create_group.html', + title='새 그룹 생성', + categories=categories) + + new_group = Group(name=name, category_id=category_id) + db.session.add(new_group) + db.session.commit() + + flash(f'{name} 그룹이 생성되었습니다.', 'success') + return redirect(url_for('admin.manage_groups')) + + categories = Category.query.all() + return render_template('admin/create_group.html', + title='새 그룹 생성', + categories=categories) + +@admin_bp.route('/groups//delete', methods=['POST']) +def delete_group(group_id): + """그룹 삭제""" + from app.models import db, Group + + if not current_user.is_authenticated or not current_user.is_admin_user(): + flash('관리자 권한이 필요합니다.', 'error') + return redirect(url_for('auth.login')) + + group = Group.query.get_or_404(group_id) + group_name = group.name + + db.session.delete(group) + db.session.commit() + + flash(f'{group_name} 그룹이 삭제되었습니다.', 'info') + return redirect(url_for('admin.manage_groups')) + +@admin_bp.route('/all-groups-data') +def all_groups_data(): + """모든 그룹의 상세 정보 (관리자 전용)""" + from app.models import Group + + if not current_user.is_authenticated or not current_user.is_admin_user(): + flash('관리자 권한이 필요합니다.', 'error') + return redirect(url_for('login')) + + groups = Group.query.all() + group_data = [] + + for group in groups: + members_data = [] + for member in group.members: + member_info = { + 'name': member.name, + 'department': member.department or '미설정', + 'blog_url': member.blog_url or '없음' + } + if member.user: + member_info.update({ + 'email': member.user.email, + 'joined_at': member.user.created_at.strftime('%Y-%m-%d') + }) + members_data.append(member_info) + + group_data.append({ + 'id': group.id, + 'name': group.name, + 'category': group.category.name, + 'member_count': len(group.members), + 'members': members_data + }) + + return render_template('admin/all_groups_data.html', + title='전체 그룹 데이터', + groups_data=group_data) \ No newline at end of file diff --git a/clubsite/app/routes/auth.py b/clubsite/app/routes/auth.py new file mode 100644 index 0000000..873ffe1 --- /dev/null +++ b/clubsite/app/routes/auth.py @@ -0,0 +1,97 @@ +# app/routes/auth.py +from flask import Blueprint, render_template, request, redirect, url_for, flash +from flask_login import login_user, logout_user, login_required, current_user +from app.models import db, User +from datetime import datetime + +auth_bp = Blueprint('auth', __name__) + +@auth_bp.route('/login', methods=['GET', 'POST']) +def login(): + """로그인""" + if current_user.is_authenticated: + return redirect(url_for('main.index')) + + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + remember = bool(request.form.get('remember')) + + user = User.query.filter_by(name=username).first() # 수정: name 필드 사용 + + if user and user.check_password(password): + login_user(user, remember=remember) + + # 관리자라면 관리자 대시보드로, 일반 사용자라면 일반 대시보드로 + if user.is_admin_user(): # 수정: 올바른 메서드명 + flash(f'관리자로 로그인되었습니다. 환영합니다, {user.name}님!', 'success') + return redirect(url_for('main.dashboard')) + else: + flash(f'로그인되었습니다. 환영합니다, {user.name}님!', 'success') + return redirect(url_for('main.dashboard')) + else: + flash('사용자명 또는 비밀번호가 올바르지 않습니다.', 'error') + + return render_template('auth/login.html', title='로그인') + +@auth_bp.route('/register', methods=['GET', 'POST']) +def register(): + """회원가입""" + if current_user.is_authenticated: + return redirect(url_for('main.index0')) + + if request.method == 'POST': + username = request.form.get('username') + email = request.form.get('email') + password = request.form.get('password') + confirm_password = request.form.get('confirm_password') + birthdate_str = request.form.get('birthdate') + + # 유효성 검사 + if not all([username, email, password, confirm_password, birthdate_str]): + flash('모든 필드를 입력해주세요.', 'error') + return render_template('auth/signup.html', title='회원가입') + + if password != confirm_password: + flash('비밀번호가 일치하지 않습니다.', 'error') + return render_template('auth/signup.html', title='회원가입') + + # 중복 확인 + if User.query.filter_by(name=username).first(): + flash('이미 존재하는 사용자명입니다.', 'error') + return render_template('auth/signup.html', title='회원가입') + + if User.query.filter_by(email=email).first(): + flash('이미 존재하는 이메일입니다.', 'error') + return render_template('auth/signup.html', title='회원가입') + + # 생년월일 변환 + try: + birthdate = datetime.strptime(birthdate_str, '%Y-%m-%d').date() + except ValueError: + flash('올바른 생년월일 형식을 입력해주세요. (YYYY-MM-DD)', 'error') + return render_template('auth/signup.html', title='회원가입') + + # 새 사용자 생성 + user = User( + name=username, + email=email, + birthdate=birthdate + ) + user.set_password(password) + + db.session.add(user) + db.session.commit() + + flash('회원가입이 완료되었습니다. 로그인해주세요.', 'success') + return redirect(url_for('auth.login')) + + return render_template('auth/signup.html', title='회원가입') + +@auth_bp.route('/logout') +@login_required +def logout(): + """로그아웃""" + logout_user() + flash('로그아웃되었습니다.', 'info') + return redirect(url_for('main.index0')) \ No newline at end of file diff --git a/clubsite/app/routes/main.py b/clubsite/app/routes/main.py new file mode 100644 index 0000000..340de6d --- /dev/null +++ b/clubsite/app/routes/main.py @@ -0,0 +1,24 @@ +from flask import Blueprint, render_template +from flask_login import login_required +from app.models import Category, Group, Member + +main_bp = Blueprint('main', __name__) + +@main_bp.route('/dashboard') +@login_required +def dashboard(): + categories = Category.query.all() + return render_template('index.html', categories=categories, title = 'main page') + + +@main_bp.route('/category/') +@login_required +def view_groups(cat_id): + groups = Group.query.filter_by(category_id=cat_id).all() + return render_template('groups.html', groups=groups) + +@main_bp.route('/group/') +@login_required +def view_members(group_id): + members = Member.query.filter_by(group_id=group_id).all() + return render_template('members.html', members=members) \ No newline at end of file diff --git a/clubsite/app/static/data/categories.csv b/clubsite/app/static/data/categories.csv new file mode 100644 index 0000000..dcdbceb --- /dev/null +++ b/clubsite/app/static/data/categories.csv @@ -0,0 +1,3 @@ +id,name +1,비기너 +2,챌린저 diff --git a/clubsite/app/static/data/groups.csv b/clubsite/app/static/data/groups.csv new file mode 100644 index 0000000..47cc42e --- /dev/null +++ b/clubsite/app/static/data/groups.csv @@ -0,0 +1,14 @@ +id,name,category_id +1,헬로 보안,1 +2,Drawhat,1 +3,네버해킹,1 +4,ascent,1 +5,과탑,1 +6,Under Construction,2 +7,FӨЯΣПƧΣΣKΣЯƧ,2 +8,𝟺𝟶𝟺𝙿𝚠𝚗𝙵𝚘𝚞𝚗𝚍,2 +9,gₕₒ₃ₜ,2 +10,QRA,2 +11,Get 4 Guitar,2 +12,LuckyCookie,2 +13,ChatSploit,2 diff --git a/clubsite/app/static/data/members.csv b/clubsite/app/static/data/members.csv new file mode 100644 index 0000000..9e7a265 --- /dev/null +++ b/clubsite/app/static/data/members.csv @@ -0,0 +1,40 @@ +id,name,department,group_id,student number,blog_url +1,유예원,사이버보안학과,1,2371089,https://www.notion.so/hwijugn/1b370ca2d94780b9bf91e9eef3d34ce7?v=1b370ca2d9478148a813000c85edb6ff&p=1c670ca2d94780e6a29cdcac5a2e35ea&pm=s +2,김은빈,영어영문학과,1,2090019,https://kimeunnen.tistory.com/ +3,박고은,사이버보안학과,1,2466020,https://pokpung-ganji.tistory.com +4,서연아,컴퓨터공학과,1,2476135,https://docs.google.com/document/d/1mEF5WvuRlFMoNaEUOKpXcFEBN1SecTxMjFEooSPGkls/edit?usp=sharing +5,박주영,사이버보안학과,1,2467012,() +6,공지영,사이버보안학과,2,2467004,https://blog.naver.com/kongjiyeong_ +7,서예인,컴퓨터공학과,2,2466029,https://todont-knowfrom0521.tistory.com/ +8,왕은서,소프트웨어학부,2,2371088,https://itsmeking.tistory.com +9,정채린,사이버보안학과,3,2467026,https://blog.naver.com/kijmhan77/223804993618 +10,황영서,컴퓨터공학과,3,2371069,https://velog.io/@kagu/posts +11,석윤서,사이버보안학과,3,2467018,https://blog.naver.com/ysseok05 +12,민채은,소프트웨어학부,3,2371080,https://velog.io/@alscodms04/posts +13,노윤하,컴퓨터공학과,4,2566014,https://nononnn.tistory.com/5 +14,박지예,사이버보안학과,4,2467013,https://m.blog.naver.com/1001zi +15,송유진,컴퓨터공학과,4,2376138,https://slytherin-1.tistory.com/2 +16,임정민,사이버보안학과,4,2371095,https://blog.naver.com/limjm0424 +17,김은아,소프트웨어학부,5,2371079,https://velog.io/@anb0o/posts +18,백소정,사이버보안학과,5,2467014,https://werty2.tistory.com/ +19,오로라,사이버보안학과,5,2467022,https://velog.io/@aroro6210/posts +20,주현아,컴퓨터공학과,5,2466055,bibimbab03 (비빔) / 작성글 - velog +21,지연경,소프트웨어학부,6,(),() +22,조휘정,사이버보안학과,6,(),https://blog.naver.com/ysseok05/223808413956 +23,박연수,소프트웨어학부,7,(),https://dustn926.tistory.com/category/연구/Deepfake로부터 안전한 사진 변환 서비스 +24,장다연,(),7,(),https://velog.io/@daniayo/series/ECOPS +25,송연우,(),8,(),https://miaami.tistory.com/ +26,최다인,소프트웨어학부,8,(),https://dada42.tistory.com/ +27,곽인정,(),9,(),https://securitystudying.tistory.com/ +28,박시원,(),9,(),() +29,이승연,(),10,(),https://blog.naver.com/seulyeon719 +30,김세정,소프트웨어학부,10,(),https://tpwjdstudy.tistory.com/ +31,장다연,(),11,(),https://velog.io/@daniayo/series/ECOPS +32,조휘정,사이버보안학과,11,(),https://blog.naver.com/ysseok05/223808413956 +33,곽인정,(),11,(),https://dada42.tistory.com/ +34,송연우,(),12,(),https://miaami.tistory.com/ +35,지연경,소프트웨어학부,12,(),() +36,이승연,(),12,(),https://blog.naver.com/seulyeon719 +37,박연수,소프트웨어학부,13,(),https://dustn926.tistory.com/category/연구/Deepfake로부터 안전한 사진 변환 서비스 +38,이승연,(),13,(),https://blog.naver.com/seulyeon719 +39,박시원,(),13,(),() diff --git a/clubsite/app/static/data/users.csv b/clubsite/app/static/data/users.csv new file mode 100644 index 0000000..90e2b33 --- /dev/null +++ b/clubsite/app/static/data/users.csv @@ -0,0 +1,41 @@ +id,name,password,is_admin,email +1,admin,password123,TRUE,123 +2,user1,pass1,FALSE,123 +3,user2,pass2,FALSE,123 +4,user3,pass3,FALSE,123 +5,user4,pass4,FALSE,123 +6,user5,pass5,FALSE,123 +7,user6,pass6,FALSE,123 +8,user7,pass7,FALSE,123 +9,user8,pass8,FALSE,123 +10,user9,pass9,FALSE,123 +11,user10,pass10,FALSE,123 +12,user11,pass11,FALSE,123 +13,user12,pass12,FALSE,123 +14,user13,pass13,FALSE,123 +15,user14,pass14,FALSE,123 +16,user15,pass15,FALSE,123 +17,user16,pass16,FALSE,123 +18,user17,pass17,FALSE,123 +19,user18,pass18,FALSE,123 +20,user19,pass19,FALSE,123 +21,user20,pass20,FALSE,123 +22,user21,pass21,FALSE,123 +23,user22,pass22,FALSE,123 +24,user23,pass23,FALSE,123 +25,user24,pass24,FALSE,123 +26,user25,pass25,FALSE,123 +27,user26,pass26,FALSE,123 +28,user27,pass27,FALSE,123 +29,user28,pass28,FALSE,123 +30,user29,pass29,FALSE,123 +31,user30,pass30,FALSE,123 +32,user31,pass31,FALSE,123 +33,user32,pass32,FALSE,123 +34,user33,pass33,FALSE,123 +35,user34,pass34,FALSE,123 +36,user35,pass35,FALSE,123 +37,user36,pass36,FALSE,123 +38,user37,pass37,FALSE,123 +39,user38,pass38,FALSE,123 +40,user39,pass39,FALSE,123 diff --git a/clubsite/app/static/index.css b/clubsite/app/static/index.css new file mode 100644 index 0000000..1cd1bba --- /dev/null +++ b/clubsite/app/static/index.css @@ -0,0 +1,48 @@ +body { + margin: 0; + padding: 0; + background-color: #000; + color: white; + font-family: 'Segoe UI', sans-serif; + display: flex; + flex-direction: column; + height: 100vh; +} + +header { + background-color: #111; + padding: 20px; + display: flex; + justify-content: space-between; + align-items: center; +} + +header h1 { + margin: 0; + font-size: 36px; +} + +.auth-links a { + color: white; + text-decoration: none; + margin-left: 15px; +} + +.container { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + gap: 100px; +} + +.category a { + font-size: 32px; + font-weight: bold; + color: #00bfff; + text-decoration: none; +} + +.category a:hover { + color: #1e90ff; +} \ No newline at end of file diff --git a/clubsite/app/static/index0.css b/clubsite/app/static/index0.css new file mode 100644 index 0000000..b1d5e57 --- /dev/null +++ b/clubsite/app/static/index0.css @@ -0,0 +1,31 @@ +body { + margin: 0; + height: 100vh; + background-color: black; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: white; + font-family: sans-serif; +} + +h1 { + font-size: 60px; + margin-bottom: 20px; +} + +.login-btn { + font-size: 20px; + color: white; + border: 2px solid white; + padding: 10px 30px; + background: none; + cursor: pointer; + border-radius: 10px; +} + +.login-btn:hover { + background-color: white; + color: black; +} \ No newline at end of file diff --git a/clubsite/app/static/login.css b/clubsite/app/static/login.css new file mode 100644 index 0000000..4b82599 --- /dev/null +++ b/clubsite/app/static/login.css @@ -0,0 +1,50 @@ +body { + background-color: #121212; + color: #ffffff; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +table { + width: 300px; + background-color: #1e1e1e; + padding: 30px; + border-radius: 12px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.7); + font-size: 15px; +} + +input[type="text"], +input[type="password"] { + width: 100%; + height: 36px; + font-size: 15px; + border: none; + border-radius: 10px; + outline: none; + padding-left: 10px; + background-color: #2a2a2a; + color: white; + margin-bottom: 15px; + box-sizing: border-box; +} + +.btn { + width: 100%; + height: 36px; + font-size: 15px; + border: none; + border-radius: 10px; + background-color: rgb(164, 199, 255); /* 기존 파스텔 블루 */ + color: black; + font-weight: bold; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.btn:active { + background-color: rgb(61, 135, 255); /* 클릭 시 더 진한 블루 */ +} diff --git a/clubsite/app/static/signup.css b/clubsite/app/static/signup.css new file mode 100644 index 0000000..c654790 --- /dev/null +++ b/clubsite/app/static/signup.css @@ -0,0 +1,77 @@ +body { + background-color: #121212; + color: #ffffff; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + display: flex; + justify-content: center; + padding: 40px; +} + +form { + background-color: #1e1e1e; + padding: 30px; + border-radius: 12px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.7); +} + +table { + width: 100%; +} + +h2 { + text-align: center; + margin-bottom: 20px; + color: white; +} + +.text, +.email { + width: 95%; + padding: 10px; + margin: 5px 0; + background-color: #2a2a2a; + color: white; + border: 1px solid #555; + border-radius: 6px; +} + +.email-container { + display: flex; + align-items: center; + gap: 8px; +} + +.at { + color: #bbb; + font-weight: bold; +} + +.check-btn { + padding: 6px 10px; + margin-left: 8px; + background-color: #444; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.check-btn:hover { + background-color: #555; +} + +.btn { + width: 100%; + padding: 12px; + background-color: rgb(164, 199, 255); + color: #000; + font-weight: bold; + border: none; + border-radius: 8px; + cursor: pointer; + margin-top: 20px; +} + +.btn:hover { + background-color: rgb(61, 135, 255); +} diff --git a/clubsite/app/templates/auth-check.js b/clubsite/app/templates/auth-check.js new file mode 100644 index 0000000..0a1fb18 --- /dev/null +++ b/clubsite/app/templates/auth-check.js @@ -0,0 +1,22 @@ +window.onload = function () { + const isLoggedIn = localStorage.getItem('loggedIn'); + if (!isLoggedIn) { + alert("로그인 후 이용 가능합니다."); + + // 절대경로 리디렉션 (항상 ECOPS_webpage 기준으로 이동) + const redirectURL = `${window.location.origin}/ECOPS_webpage/index0.html`; + window.location.href = redirectURL; + return; + } + + // 로그인 상태면 본문 표시 + document.body.style.display = "block"; + + // 새로고침 시 로그아웃 + const navType = performance.getEntriesByType("navigation")[0]?.type; + if (navType === "reload") { + localStorage.removeItem('loggedIn'); + const redirectURL = `${window.location.origin}/ECOPS_webpage/index0.html`; + window.location.href = redirectURL; + } +}; diff --git a/clubsite/app/templates/auth/login.html b/clubsite/app/templates/auth/login.html new file mode 100644 index 0000000..b357ad6 --- /dev/null +++ b/clubsite/app/templates/auth/login.html @@ -0,0 +1,65 @@ + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + +
+

로그인

+
로그인 정보 저장
회원가입
+
+ + + \ No newline at end of file diff --git a/clubsite/app/templates/auth/logout.html b/clubsite/app/templates/auth/logout.html new file mode 100644 index 0000000..5ef7e60 --- /dev/null +++ b/clubsite/app/templates/auth/logout.html @@ -0,0 +1,17 @@ + + + + + 로그아웃 + + + + + diff --git a/clubsite/app/templates/auth/signup.html b/clubsite/app/templates/auth/signup.html new file mode 100644 index 0000000..51f4618 --- /dev/null +++ b/clubsite/app/templates/auth/signup.html @@ -0,0 +1,108 @@ + + + + + + + Join + + + + +
+ + + + + + + + + + + + + + + + + +

회원가입

아이디
+
+ + +
+
비밀번호
비밀번호 확인
이름
이메일
+
+ + + + diff --git a/clubsite/app/templates/category/beginner.html b/clubsite/app/templates/category/beginner.html new file mode 100644 index 0000000..feda53a --- /dev/null +++ b/clubsite/app/templates/category/beginner.html @@ -0,0 +1,18 @@ + + + + + + Beginner 그룹 목록 + + +

Beginner 그룹 목록

+ + + diff --git a/clubsite/app/templates/category/challenger.html b/clubsite/app/templates/category/challenger.html new file mode 100644 index 0000000..5b44f3e --- /dev/null +++ b/clubsite/app/templates/category/challenger.html @@ -0,0 +1,15 @@ + + + + + + Challenger Groups + + + +

Challenger 그룹 목록

+ + + + \ No newline at end of file diff --git a/clubsite/app/templates/errors/403.html b/clubsite/app/templates/errors/403.html new file mode 100644 index 0000000..b6392a2 --- /dev/null +++ b/clubsite/app/templates/errors/403.html @@ -0,0 +1,15 @@ + + + + + 403 Forbidden + + + +
+

403

+

접근이 금지된 페이지입니다.

+ 홈으로 돌아가기 +
+ + diff --git a/clubsite/app/templates/errors/404.html b/clubsite/app/templates/errors/404.html new file mode 100644 index 0000000..d415d0c --- /dev/null +++ b/clubsite/app/templates/errors/404.html @@ -0,0 +1,15 @@ + + + + + 404 Not Found + + + +
+

404

+

페이지를 찾을 수 없습니다.

+ 홈으로 돌아가기 +
+ + diff --git a/clubsite/app/templates/errors/500.html b/clubsite/app/templates/errors/500.html new file mode 100644 index 0000000..2ebc261 --- /dev/null +++ b/clubsite/app/templates/errors/500.html @@ -0,0 +1,15 @@ + + + + + 500 Internal Server Error + + + +
+

500

+

서버에 문제가 발생했습니다. 나중에 다시 시도해주세요.

+ 홈으로 돌아가기 +
+ + diff --git a/clubsite/app/templates/index.html b/clubsite/app/templates/index.html new file mode 100644 index 0000000..880083f --- /dev/null +++ b/clubsite/app/templates/index.html @@ -0,0 +1,52 @@ + + + + + + E-COPS + + + + + + +
+

E-COPS

+ +
+
+
+ BEGINNER +
+ +
+ + + + \ No newline at end of file diff --git a/clubsite/app/templates/index0.html b/clubsite/app/templates/index0.html new file mode 100644 index 0000000..c6c0e19 --- /dev/null +++ b/clubsite/app/templates/index0.html @@ -0,0 +1,16 @@ + + + + + + E-COPS + + + + + +

E-COPS

+ + + + \ No newline at end of file diff --git a/clubsite/app/templates/team/group.html b/clubsite/app/templates/team/group.html new file mode 100644 index 0000000..02e31ac --- /dev/null +++ b/clubsite/app/templates/team/group.html @@ -0,0 +1,14 @@ + + + + + + {{ group.name }} - 그룹 멤버 + + + +

{{ group.name }} - 멤버 목록

+ + + + \ No newline at end of file diff --git a/clubsite/app/templates/team/member.html b/clubsite/app/templates/team/member.html new file mode 100644 index 0000000..2d06ced --- /dev/null +++ b/clubsite/app/templates/team/member.html @@ -0,0 +1,16 @@ + + + + + + {{ member.name }} + + + +

{{ member.name }}

+

- 학과: {{ member.department }}

+

- 학번: {{ member['student number'] }}

+

- 블로그: {{ member.blog_url }}

+ + + \ No newline at end of file diff --git a/clubsite/app/templates/unauthorized.html b/clubsite/app/templates/unauthorized.html new file mode 100644 index 0000000..3015be8 --- /dev/null +++ b/clubsite/app/templates/unauthorized.html @@ -0,0 +1,36 @@ + + + + + + + + + 권한 대기 중 + + + + +
+ 현재 귀하의 계정은 관리자 승인을 기다리고 있습니다.
+ 로그인은 승인 후 가능하며, Seed값이 필요합니다. +
+ + + diff --git a/clubsite/config.py b/clubsite/config.py new file mode 100644 index 0000000..cec33ae --- /dev/null +++ b/clubsite/config.py @@ -0,0 +1,6 @@ +import os + +class Config: + SECRET_KEY = os.getenv('SECRET_KEY', 'devkey') + SQLALCHEMY_DATABASE_URI = 'sqlite:///club.db' + SQLALCHEMY_TRACK_MODIFICATIONS = False \ No newline at end of file diff --git a/clubsite/flaskenv b/clubsite/flaskenv new file mode 100644 index 0000000..7828951 --- /dev/null +++ b/clubsite/flaskenv @@ -0,0 +1,3 @@ +FLASK_APP=app +FLASK_ENV=development +FLASK_RUN_PORT=5000 \ No newline at end of file diff --git a/clubsite/instance/club.db b/clubsite/instance/club.db new file mode 100644 index 0000000..d1cc458 Binary files /dev/null and b/clubsite/instance/club.db differ diff --git a/clubsite/requirements.txt b/clubsite/requirements.txt new file mode 100644 index 0000000..c2055aa Binary files /dev/null and b/clubsite/requirements.txt differ diff --git a/clubsite/run.py b/clubsite/run.py new file mode 100644 index 0000000..7576230 --- /dev/null +++ b/clubsite/run.py @@ -0,0 +1,37 @@ +# run.py +from app import create_app + +app = create_app() + +if __name__ == '__main__': + # app context 내에서 DB 관련 작업 + with app.app_context(): + from app.models import db, User, Category + + db.create_all() + + # 기본 카테고리 생성 + if not Category.query.first(): + beginner = Category(name='비기너') + challenger = Category(name='챌린저') + db.session.add(beginner) + db.session.add(challenger) + db.session.commit() + print("기본 카테고리가 생성되었습니다.") + + # 관리자 계정 생성 + admin = User.query.filter_by(is_admin=True).first() + if not admin: + from datetime import date + admin_user = User( + name='admin', + email='admin@club.com', + is_admin=True, + birthdate=date(1990, 1, 1) + ) + admin_user.set_password('admin123') + db.session.add(admin_user) + db.session.commit() + print("관리자 계정이 생성되었습니다. (admin/admin123)") + + app.run(debug=True) \ No newline at end of file diff --git a/clubsite/seed.py b/clubsite/seed.py new file mode 100644 index 0000000..7d17d59 --- /dev/null +++ b/clubsite/seed.py @@ -0,0 +1,55 @@ +import csv +import os +from app import db +from app.models import User, Category, Group, Member +from werkzeug.security import generate_password_hash + +# CSV 파일 경로 설정 +CSV_DIR = os.path.join('app', 'static', 'data') + +def load_users(): + with open(os.path.join(CSV_DIR, 'users.csv'), encoding='utf-8') as f: + reader = csv.DictReader(f) + for row in reader: + user = User( + id=row['id'], + name=row['name'], + password_hash=generate_password_hash(row['password']), + is_admin=row['is_admin'].strip().lower() in ['1', 'true', 'yes'] + ) + db.session.add(user) + +def load_categories(): + with open(os.path.join(CSV_DIR, 'categories.csv'), encoding='utf-8') as f: + reader = csv.DictReader(f) + for row in reader: + db.session.add(Category(id=row['id'], name=row['name'])) + +def load_groups(): + with open(os.path.join(CSV_DIR, 'groups.csv'), encoding='utf-8') as f: + reader = csv.DictReader(f) + for row in reader: + db.session.add(Group(id=row['id'], name=row['name'], category_id=row['category_id'])) + +def load_members(): + with open(os.path.join(CSV_DIR, 'members.csv'), encoding='utf-8') as f: + reader = csv.DictReader(f) + for row in reader: + db.session.add(Member( + id=row['id'], + name=row['name'], + department=row['department'], + blog_url=row['blog_url'], + group_id=row['group_id'] + )) + +def main(): + load_categories() + load_groups() + load_members() + load_users() + db.session.commit() + +if __name__ == '__main__': + main() + db.create_all() \ No newline at end of file diff --git a/template/index.html b/template/index.html new file mode 100644 index 0000000..ad60a6e --- /dev/null +++ b/template/index.html @@ -0,0 +1,10 @@ + + + + 홈페이지 + + +

Hello, Flask!

+

수정자: 서예인

+ +