@@ -1,326 +0,0 @@
< template >
< div class = "alliance-panel" >
< div class = "panel-header" @click ="expanded = !expanded" >
< span > ⚔ ️ 연맹원 목록 < / span >
< span class = "toggle-arrow" : class = "{ open: expanded }" > ▾ < / span >
< / div >
< div v-if = "expanded" class="panel-body" >
< div v-if = "!authed" class="auth-section" >
< div class = "field-row" >
< select v-model = "selectedAllianceId" class="select-input" :disabled="loadingAlliances" >
< option value = "" > 연맹 선택 ... < / option >
< option v-for = "a in alliances" :key="a.id" :value="a.id" >
[ {{ a.tag }} ] {{ a.name }} ( 서버 {{ a.server_number }} )
< / option >
< / select >
< / div >
< div class = "field-row" >
< input v-model = "pin" type="password" class="pin-input" placeholder="R5 PIN 입력" @keyup.enter="doAuth" / >
< button class = "btn-auth" @click ="doAuth" : disabled = "authing || !selectedAllianceId || !pin.trim()" >
{ { authing ? '확인중...' : '확인' } }
< / button >
< / div >
< div v-if = "authError" class="error-msg" > {{ authError }} < / div >
< / div >
< div v-else class = "members-section" >
< div class = "members-header" >
< span class = "alliance-badge" > ⚔ ️ { { currentAlliance ? . tag } } { { currentAlliance ? . name } } < / span >
< div class = "header-actions" >
< input v-model = "memberSearch" class="search-input" placeholder="닉네임 검색..." / >
< button class = "btn-logout" @click ="logout" > 나가기 < / button >
< / div >
< / div >
< div class = "role-filter" >
< button
v-for = "r in roleFilters"
:key = "r"
: class = "['role-btn', { active: selectedRole === r }]"
@click ="selectedRole = r"
> { { r } } < / button >
< / div >
< div class = "members-stats" >
< span class = "stat" > 전체 { { filteredMembers . length } } 명 < / span >
< span class = "stat linked" > ✅ 쿠폰 등록 : { { linkedCount } } 명 < / span >
< span class = "stat unlinked" > ⚠ ️ 미등록 : { { filteredMembers . length - linkedCount } } 명 < / span >
< / div >
< div class = "member-list" >
< div
v-for = "m in filteredMembers"
:key = "m.id"
: class = "['member-row', { linked: m.in_coupon_system }]"
>
< span : class = "['role-tag', roleClass(m.role)]" > { { m . role } } < / span >
< span class = "unit-icon" > { { unitIcon ( m . main _unit ) } } < / span >
< span class = "nickname" > { { m . nickname } } < / span >
< span class = "power" > { { formatPower ( m . power ) } } < / span >
< span v-if = "m.in_coupon_system" class="coupon-badge" > 쿠폰 등록됨 < / span >
< span v-else class = "coupon-miss" > 미등록 < / span >
< / div >
< div v-if = "filteredMembers.length === 0" class="empty" > 검색 결과가 없습니다 < / div >
< / div >
< / div >
< / div >
< / div >
< / template >
< script setup lang = "ts" >
import { ref , computed , onMounted } from 'vue'
import axios from 'axios'
interface Alliance {
id : number
server _number : number
tag : string
name : string
}
interface AllianceMember {
id : number
nickname : string
role : string
main _unit : string
power : number
in _coupon _system : boolean
}
const expanded = ref ( false )
const alliances = ref < Alliance [ ] > ( [ ] )
const loadingAlliances = ref ( false )
const selectedAllianceId = ref < number | ' ' > ( '' )
const pin = ref ( '' )
const authing = ref ( false )
const authError = ref ( '' )
const authed = ref ( false )
const currentAlliance = ref < Alliance | null > ( null )
const members = ref < AllianceMember [ ] > ( [ ] )
const memberSearch = ref ( '' )
const selectedRole = ref ( '전체' )
const roleFilters = [ '전체' , 'R5' , 'R4' , 'R3' , 'R2' , 'R1' ]
const filteredMembers = computed ( ( ) => {
let list = members . value
if ( selectedRole . value !== '전체' ) list = list . filter ( ( m ) => m . role === selectedRole . value )
const q = memberSearch . value . trim ( ) . toLowerCase ( )
if ( q ) list = list . filter ( ( m ) => m . nickname . toLowerCase ( ) . includes ( q ) )
return list
} )
const linkedCount = computed ( ( ) => filteredMembers . value . filter ( ( m ) => m . in _coupon _system ) . length )
onMounted ( async ( ) => {
loadingAlliances . value = true
try {
const { data } = await axios . get ( '/api/alliances' )
alliances . value = data
} finally {
loadingAlliances . value = false
}
} )
async function doAuth ( ) {
if ( ! selectedAllianceId . value || ! pin . value . trim ( ) ) return
authing . value = true
authError . value = ''
try {
const { data } = await axios . post ( '/api/alliance-members' , {
alliance _id : selectedAllianceId . value ,
pin : pin . value . trim ( ) ,
} )
members . value = data
currentAlliance . value = alliances . value . find ( ( a ) => a . id === selectedAllianceId . value ) ? ? null
authed . value = true
} catch ( e : any ) {
authError . value = e ? . response ? . data ? . message || 'PIN이 틀렸거나 오류가 발생했습니다'
} finally {
authing . value = false
}
}
function logout ( ) {
authed . value = false
members . value = [ ]
pin . value = ''
authError . value = ''
currentAlliance . value = null
}
function roleClass ( role : string ) {
return { R5 : 'r5' , R4 : 'r4' , R3 : 'r3' , R2 : 'r2' , R1 : 'r1' } [ role ] ? ? ''
}
function unitIcon ( u : string ) {
return { infantry : '🛡' , lancer : '🗡' , marksman : '🏹' , mixed : '⚔️' } [ u ] ? ? '⚔️'
}
function formatPower ( p : number ) {
if ( ! p ) return '-'
if ( p >= 1 _000 _000 _000 ) return ( p / 1 _000 _000 _000 ) . toFixed ( 1 ) + 'B'
if ( p >= 1 _000 _000 ) return ( p / 1 _000 _000 ) . toFixed ( 1 ) + 'M'
if ( p >= 1 _000 ) return ( p / 1 _000 ) . toFixed ( 1 ) + 'K'
return String ( p )
}
< / script >
< style scoped >
. alliance - panel {
background : # 1 e1e2e ;
border : 1 px solid # 2 a2a3e ;
border - radius : 12 px ;
overflow : hidden ;
}
. panel - header {
display : flex ;
align - items : center ;
justify - content : space - between ;
padding : 14 px 18 px ;
cursor : pointer ;
font - weight : 700 ;
font - size : 0.95 rem ;
color : # cdd6f4 ;
user - select : none ;
transition : background 0.1 s ;
}
. panel - header : hover { background : # 252535 ; }
. toggle - arrow { font - size : 0.85 rem ; color : # 6 c7086 ; transition : transform 0.2 s ; }
. toggle - arrow . open { transform : rotate ( 180 deg ) ; }
. panel - body { padding : 16 px ; }
. auth - section { display : flex ; flex - direction : column ; gap : 10 px ; }
. field - row { display : flex ; gap : 8 px ; }
. select - input {
flex : 1 ;
background : # 181825 ;
border : 1.5 px solid # 313244 ;
color : # cdd6f4 ;
border - radius : 8 px ;
padding : 9 px 12 px ;
font - size : 0.88 rem ;
}
. select - input : focus { outline : none ; border - color : # 89 b4fa ; }
. pin - input {
flex : 1 ;
background : # 181825 ;
border : 1.5 px solid # 313244 ;
color : # cdd6f4 ;
border - radius : 8 px ;
padding : 9 px 12 px ;
font - size : 0.88 rem ;
}
. pin - input : focus { outline : none ; border - color : # 89 b4fa ; }
. btn - auth {
background : # 1 e3a8a ;
color : # 93 c5fd ;
border : none ;
border - radius : 8 px ;
padding : 9 px 16 px ;
font - size : 0.85 rem ;
font - weight : 700 ;
cursor : pointer ;
}
. btn - auth : hover : not ( : disabled ) { background : # 1 d4ed8 ; }
. btn - auth : disabled { opacity : 0.4 ; cursor : not - allowed ; }
. error - msg { color : # f38ba8 ; font - size : 0.82 rem ; padding : 4 px 0 ; }
. members - section { display : flex ; flex - direction : column ; gap : 12 px ; }
. members - header {
display : flex ;
align - items : center ;
justify - content : space - between ;
gap : 10 px ;
flex - wrap : wrap ;
}
. alliance - badge { font - weight : 700 ; color : # 89 b4fa ; font - size : 0.9 rem ; }
. header - actions { display : flex ; gap : 8 px ; align - items : center ; }
. search - input {
background : # 181825 ;
border : 1.5 px solid # 313244 ;
color : # cdd6f4 ;
border - radius : 8 px ;
padding : 7 px 12 px ;
font - size : 0.85 rem ;
width : 150 px ;
}
. search - input : focus { outline : none ; border - color : # 89 b4fa ; }
. btn - logout {
background : transparent ;
color : # 6 c7086 ;
border : 1 px solid # 313244 ;
border - radius : 8 px ;
padding : 6 px 12 px ;
font - size : 0.8 rem ;
cursor : pointer ;
}
. btn - logout : hover { color : # f38ba8 ; border - color : # f38ba8 ; }
. role - filter { display : flex ; gap : 6 px ; flex - wrap : wrap ; }
. role - btn {
background : # 181825 ;
border : 1.5 px solid # 313244 ;
color : # 6 c7086 ;
border - radius : 20 px ;
padding : 4 px 12 px ;
font - size : 0.78 rem ;
font - weight : 700 ;
cursor : pointer ;
}
. role - btn : hover { border - color : # 89 b4fa ; color : # 89 b4fa ; }
. role - btn . active { background : # 1 e3a5f ; border - color : # 89 b4fa ; color : # 89 b4fa ; }
. members - stats { display : flex ; gap : 12 px ; font - size : 0.8 rem ; }
. stat { color : # 6 c7086 ; }
. stat . linked { color : # a6e3a1 ; }
. stat . unlinked { color : # fab387 ; }
. member - list { display : flex ; flex - direction : column ; gap : 2 px ; max - height : 340 px ; overflow - y : auto ; }
. member - row {
display : flex ;
align - items : center ;
gap : 8 px ;
padding : 8 px 10 px ;
border - radius : 8 px ;
background : # 181825 ;
font - size : 0.85 rem ;
transition : background 0.1 s ;
}
. member - row . linked { background : # 0 f2010 ; }
. member - row : hover { background : # 252535 ; }
. member - row . linked : hover { background : # 152 a15 ; }
. role - tag {
padding : 2 px 7 px ;
border - radius : 10 px ;
font - size : 0.72 rem ;
font - weight : 800 ;
min - width : 28 px ;
text - align : center ;
}
. role - tag . r5 { background : # 3 b1f00 ; color : # fab387 ; }
. role - tag . r4 { background : # 1 e1b4b ; color : # c4b5fd ; }
. role - tag . r3 { background : # 1 e2d3e ; color : # 89 b4fa ; }
. role - tag . r2 { background : # 1 c2a1c ; color : # a6e3a1 ; }
. role - tag . r1 { background : # 252535 ; color : # 6 c7086 ; }
. unit - icon { font - size : 0.9 rem ; }
. nickname { flex : 1 ; color : # cdd6f4 ; font - weight : 600 ; }
. power { color : # 6 c7086 ; font - size : 0.8 rem ; min - width : 50 px ; text - align : right ; }
. coupon - badge {
background : # 132010 ;
color : # a6e3a1 ;
padding : 2 px 8 px ;
border - radius : 10 px ;
font - size : 0.72 rem ;
font - weight : 700 ;
}
. coupon - miss {
color : # 6 c7086 ;
font - size : 0.72 rem ;
}
. empty { color : # 6 c7086 ; text - align : center ; padding : 16 px 0 ; font - size : 0.85 rem ; }
< / style >